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