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