001package jmri.jmrix.bidib;
002
003import java.util.ArrayList;
004import java.util.HashMap;
005import java.util.Locale;
006import java.util.Map;
007import jmri.JmriException;
008import jmri.Sensor;
009
010import org.bidib.jbidibc.messages.BidibLibrary;
011import org.bidib.jbidibc.messages.Node;
012import org.bidib.jbidibc.messages.message.FeedbackGetRangeMessage;
013
014import org.slf4j.Logger;
015import org.slf4j.LoggerFactory;
016
017/**
018 * Implement SensorManager for BiDiB systems.
019 * <p>
020 *
021 * @author Bob Jacobsen Copyright (C) 2008
022 * @author Eckart Meyer Copyright (C) 2019-2023
023 */
024public class BiDiBSensorManager extends jmri.managers.AbstractSensorManager {
025
026    // Whether we accumulate partially loaded turnouts in pendingTurnouts.
027    private boolean isLoading = false;
028    // Turnouts that are being loaded from XML.
029    private final ArrayList<BiDiBSensor> pendingSensors = new ArrayList<>();
030    private final Map<Node, Integer> pendingNodeMinAddr = new HashMap<>();
031    private final Map<Node, Integer> pendingNodeMaxAddr = new HashMap<>();
032
033    public BiDiBSensorManager(BiDiBSystemConnectionMemo memo) {
034        super(memo);
035    }
036
037    /**
038     * {@inheritDoc}
039     */
040    @Override
041    public BiDiBSystemConnectionMemo getMemo() {
042        return (BiDiBSystemConnectionMemo) memo;
043    }
044
045    /**
046     * {@inheritDoc}
047     */
048    @Override
049    public void dispose() {
050        super.dispose();
051    }
052
053    // BiDiB-specific methods
054    /**
055     * {@inheritDoc}
056     */
057    @Override
058    public Sensor createNewSensor(String systemName, String userName) {
059        log.trace("createNewSensor {} - {}", systemName, userName);
060        //String addr = systemName.substring(getSystemPrefix().length() + 1);
061        // first, check validity
062        try {
063            validateSystemNameFormat(systemName);
064        } catch (IllegalArgumentException e) {
065            log.error("Illegal address", e);
066            throw e;
067        }
068        // OK, make
069        BiDiBSensor s = new BiDiBSensor(systemName, this);
070        s.setUserName(userName);
071
072        synchronized (pendingSensors) {
073            if (isLoading) {
074                pendingSensors.add(s);
075                if (s.getAddr().isFeedbackAddr()) {
076                    // try to build minimum/maximum address to use bulk query later
077                    BiDiBAddress a = s.getAddr();
078                    Node node = a.getNode();
079                    if (!pendingNodeMinAddr.containsKey(node)  ||  a.getAddr() < pendingNodeMinAddr.get(node)) {
080                        pendingNodeMinAddr.put(node, a.getAddr());
081                    }
082                    if (!pendingNodeMaxAddr.containsKey(node)  ||  a.getAddr() > pendingNodeMaxAddr.get(node)) {
083                        pendingNodeMaxAddr.put(node, a.getAddr());
084                    }
085                }
086            } else {
087                s.finishLoad();
088            }
089        }
090
091        return s;
092    }
093
094    /**
095     * This function is invoked before an XML load is started. We defer initialization of the
096     * newly created turnouts until finishLoad because the feedback type might be changing as we
097     * are parsing the XML.
098     */
099    public void startLoad() {
100        log.debug("Sensor manager : start load");
101        synchronized (pendingSensors) {
102            isLoading = true;
103        }
104    }
105
106    /**
107     * This function is invoked after the XML load is complete and all Sensors are instantiated
108     * and their type is read in. We use this hook to finalize the construction of the
109     * objects whose instantiation was deferred until the feedback type was known.
110     */
111    public void finishLoad() {
112        log.info("Sensor manager : finish load");
113        synchronized (pendingSensors) {
114            pendingSensors.forEach((s) -> {
115                if (!s.getAddr().isFeedbackAddr()) {
116                    // sensor is not a feedback (BiDiB BM), may be a port input
117                    s.finishLoad();
118                }
119            });
120            // now request feedbacks as bulk request from each node
121            pendingNodeMinAddr.forEach((node, min) -> {
122                    updateNodeFeedbacks(node, min, pendingNodeMaxAddr.get(node));
123            });
124            pendingNodeMinAddr.clear();
125            pendingNodeMaxAddr.clear();
126            pendingSensors.clear();
127            isLoading = false;
128        }
129    }
130    
131    public void updateNodeFeedbacks(Node node) {
132        updateNodeFeedbacks(node, 0, 128);
133    }
134    
135    public void updateNodeFeedbacks(Node node, int min, int max) {
136        BiDiBTrafficController tc = getMemo().getBiDiBTrafficController();
137        int bmSize = tc.getNodeFeature(node, BidibLibrary.FEATURE_BM_SIZE);
138        if (bmSize > 0) {
139            int first = (min / 8) * 8;
140            //int max = pendingNodeMaxAddr.get(node);
141            if (max > (bmSize - 1)) {
142                max = (bmSize - 1);
143            }
144            int end = ((max + 8) / 8) * 8; //exclusive end address
145            log.debug("sensor finish load: node: {}, requesting feedback from {} to {}", node, first, end);
146            tc.sendBiDiBMessage(new FeedbackGetRangeMessage(first, end), node);
147        }
148    }
149
150    /**
151     * {@inheritDoc}
152     */
153    @Override
154    public String createSystemName(String curAddress, String prefix) throws JmriException {
155        log.trace("createSystemName from {} - {}", curAddress, prefix);
156        try {
157            int i = 1;
158            int curNum = Integer.parseInt(curAddress);
159            for (Sensor s : getNamedBeanSet()) {
160                //log.trace("turnout: {}/{} {}", i, curNum, s.getSystemName());
161                if (i++ == curNum) {
162                    return s.getSystemName();
163                }
164            }
165        } catch (java.lang.NumberFormatException ex) {
166            throw new JmriException("Hardware Address passed "+curAddress+" should be a number");
167        }
168//        // first, check validity
169//        try {
170//            validateAddressFormat(curAddress);
171//        } catch (IllegalArgumentException e) {
172//            throw new JmriException(e.toString());
173//        }
174//        // getSystemPrefix() unsigned int with "+" as service to user
175//        String newAddress = CbusAddress.validateSysName(curAddress);
176//        return prefix + typeLetter() + newAddress;
177        return prefix + typeLetter() + curAddress;
178    }
179
180    /**
181     * {@inheritDoc}
182     */
183    @Override
184    public boolean allowMultipleAdditions(String systemName) {
185        return true;
186    }
187
188    /**
189     * {@inheritDoc}
190     */
191    @Override
192    public String validateSystemNameFormat(String name, Locale locale) {
193        log.trace("validateSystemNameFormat: name: {}, typeLetter: {}", name, typeLetter());
194        validateSystemNamePrefix(name, locale);
195        //validateAddressFormat(name.substring(getSystemNamePrefix().length()));
196        if (!BiDiBAddress.isValidSystemNameFormat(name, typeLetter(), getMemo())) {
197            throw new jmri.NamedBean.BadSystemNameException(Locale.getDefault(), "InvalidSystemName",name);
198        }
199        return name;
200    }
201
202    /**
203     * {@inheritDoc}
204     */
205    @Override
206    public NameValidity validSystemNameFormat(String systemName) {
207        log.trace("validSystemNameFormat: systemNname: {}", systemName);
208        
209        if (systemName.length() <= getSystemPrefix().length()) {
210            return NameValidity.INVALID;
211        }
212        
213//        try {
214//            validateAddressFormat(addr);
215//        } catch (IllegalArgumentException e) {
216//            return NameValidity.INVALID;
217//        }
218        return NameValidity.VALID;
219    }
220
221    /**
222     * Work out the details for BiDiB hardware address validation. Logging of
223     * handled cases no higher than WARN.
224     *
225     * @param address the hardware address to check
226     * @throws IllegalArgumentException when delimiter is not found
227     */
228    //TODO!
229//    void validateAddressFormat(String address) throws IllegalArgumentException {
230//        String newAddress = CbusAddress.validateSysName(address);
231//        log.debug("validated system name {}", newAddress);
232//    }
233
234    /**
235     * {@inheritDoc}
236     */
237    @Override
238    public String getEntryToolTip() {
239        return Bundle.getMessage("AddInputEntryToolTip");
240    }
241
242    /*
243     * {@inheritDoc} Send a query message to get all sensors.
244     */
245/* NOT USED
246    @Override
247    public void updateAll() {
248        BiDiBTrafficController tc = getMemo().getBiDiBTrafficController();
249        BidibRequestFactory rf = tc.getBidib().getRootNode().getRequestFactory();
250        tc.getNodeList().forEach( (uid, node) -> {
251            int bmSize = tc.getNodeFeature(node, BidibLibrary.FEATURE_BM_SIZE);
252            if (NodeUtils.hasFeedbackFunctions(node.getUniqueId())  &&  bmSize > 0 ) {
253                log.info("Requesting feedback status on node {}", node);
254//                tc.sendBiDiBMessage(new FeedbackGetRangeMessage(0, 128), node);
255                tc.sendBiDiBMessage(new FeedbackGetRangeMessage(0, bmSize), node);
256            }
257            Feature f = tc.findNodeFeature(node, BidibLibrary.FEATURE_CTRL_INPUT_COUNT);
258            if (NodeUtils.hasSwitchFunctions(node.getUniqueId())  &&  (f == null  ||  f.getValue() > 0) ) {
259                log.info("Requesting input port status on node {}", node);
260                if (node.getProtocolVersion().isHigherThan(ProtocolVersion.VERSION_0_6)) {
261                    // fast bulk query of all ports (new in bidib protocol version 0.7)
262                    BidibCommandMessage m = (BidibCommandMessage)rf.createPortQueryAll(1 << BidibLibrary.BIDIB_PORTTYPE_INPUT, 0x0000, 0xFFFF);
263                    tc.sendBiDiBMessage(m, node);
264                }
265                else {
266                    // old version - request every single sensor
267                    getNamedBeanSet().forEach((nb) -> {
268                        if (nb instanceof BiDiBSensor) {
269                            BiDiBAddress addr = new BiDiBAddress(((BiDiBSensor) nb).getSystemName(), typeLetter(), getMemo());
270                            if (addr.isValid()  &&  addr.isPortAddr()  &&  addr.getNode().equals(node)) {
271                                BidibCommandMessage m = (BidibCommandMessage)rf.createLcPortQuery(tc.getPortModel(node), LcOutputType.INPUTPORT, addr.getAddr());
272                                log.trace("...from port {}", addr.getAddr());
273                                tc.sendBiDiBMessage(m, node);
274                            }
275                        }
276                    });
277                }
278            }
279        });
280//        getNamedBeanSet().forEach((nb) -> {
281//            if (nb instanceof CbusSensor) {
282//                nb.requestUpdateFromLayout();
283//            }
284//        });
285    }
286*/
287    
288
289    private final static Logger log = LoggerFactory.getLogger(BiDiBSensorManager.class);
290
291}