001package jmri.jmrix.bidib;
002
003import java.util.Map;
004import java.util.LinkedList;
005import java.util.SortedSet;
006
007import jmri.InstanceManager;
008
009import org.bidib.jbidibc.core.BidibInterface;
010import org.bidib.jbidibc.core.node.BidibNode;
011import org.bidib.jbidibc.core.node.RootNode;
012import org.bidib.jbidibc.messages.BidibLibrary;
013import org.bidib.jbidibc.messages.Feature;
014import org.bidib.jbidibc.messages.FeatureData;
015import org.bidib.jbidibc.messages.Node;
016import org.bidib.jbidibc.messages.StringData;
017import org.bidib.jbidibc.messages.enums.CommandStationState;
018import org.bidib.jbidibc.messages.exception.ProtocolException;
019import org.bidib.jbidibc.messages.message.CommandStationSetStateMessage;
020import org.bidib.jbidibc.messages.utils.ByteUtils;
021
022import org.slf4j.Logger;
023import org.slf4j.LoggerFactory;
024
025/**
026 * This class initializes or deinitializes a BiDiB node when it is found on system startup or if it
027 * is discovered or lost while the system is running.\
028 *
029 * The real work is done in its own thread and from a node queue. Initializing is a time consuming
030 * process since a lot of data is read from the node.
031 * 
032 * @author Eckart Meyer Copyright (C) 2023
033 */
034public class BiDiBNodeInitializer implements Runnable {
035
036    private static class SimplePair {
037        public Node node;
038        public boolean isNewNode; //true: new node, false: node lost
039        
040        public SimplePair(Node node, boolean isNewNode) {
041            this.node = node;
042            this.isNewNode = isNewNode;
043        }
044    }
045    
046    private final Map<Long, Node> nodes;
047    private final BidibInterface bidib;
048    private final BiDiBTrafficController tc;
049    private SimplePair currentNode;
050    private Thread initThread;
051    private final LinkedList<SimplePair> queue;
052    
053    
054    public BiDiBNodeInitializer(BiDiBTrafficController tc, BidibInterface bidib, Map<Long, Node> nodes) {
055        this.bidib = bidib;
056        this.nodes = nodes;
057        this.tc = tc;
058        queue = new LinkedList<>();
059        log.debug("BiDiB node initializer created");
060    }
061    
062    /**
063     * Get everything we need from the node. The node must already be inserted into the BiDiB node list.
064     * 
065     * @param node node to initialize
066     * @throws ProtocolException when features can't be loaded
067     */
068    public void initNode(Node node) throws ProtocolException {
069        if (node != null) {
070            BidibNode bidibNode = bidib.getNode(node);
071            log.info("+++ found node: {}", node);
072
073            int magic = bidibNode.getMagic(0);
074            log.debug("Node returned magic: 0x{}", ByteUtils.magicToHex(magic));
075            if (magic == 0xAFFE) {
076                node.setStoredString(StringData.INDEX_PRODUCTNAME, bidibNode.getString(0, StringData.INDEX_PRODUCTNAME).getValue());
077                node.setStoredString(StringData.INDEX_USERNAME, bidibNode.getString(StringData.NAMESPACE_NODE, StringData.INDEX_USERNAME).getValue());
078                node.setProtocolVersion(bidibNode.getProtocolVersion());
079                node.setSoftwareVersion(bidibNode.getSwVersion());
080                log.info("Product name: {}", node.getStoredString(StringData.INDEX_PRODUCTNAME));
081                log.info("User name: {}", node.getStoredString(StringData.INDEX_USERNAME));
082                log.info("Protocol version: {}", node.getProtocolVersion());
083                log.info("Software version: {}", node.getSoftwareVersion());
084
085                try {
086                    FeatureData features = bidibNode.getFeaturesAll();
087                    log.info("featureCount: {}", features.getFeatureCount());
088                    if (features.isStreamingSupport()) {
089                        int k = 1;//counter is for debug only
090                        for (Feature feature : features.getFeatures()) {
091                            log.trace("feature #{}/{}", k++, features.getFeatureCount());
092                            log.info("feature.type: {}, value: {}, name: {}", feature.getType(), feature.getValue(), feature.getFeatureName());
093                            node.setFeature(feature);
094                        }
095                    }
096                    else {
097                        Feature feature;
098                        int k = 1;//counter is for debug only
099                        try {
100                            while ((feature = bidibNode.getNextFeature()) != null) {
101                                log.trace("feature #{}/{}", k++, features.getFeatureCount());
102                                log.info("feature.type: {}, value: {}, name: {}", feature.getType(), feature.getValue(), feature.getFeatureName());
103                                node.setFeature(feature);
104                            }
105                        }
106                        catch (ProtocolException ex) {
107                            log.debug("No more features.");
108                        }
109                    }
110                }
111                catch (ProtocolException ex) {
112                    log.error("Features can't be loaded from node: {}", ex.getMessage());
113                }
114                log.info("Finished query features."); // NOSONAR
115
116                node.setFeature(new Feature(BidibLibrary.FEATURE_ACCESSORY_MACROMAPPED, 0)); //we do not handle macros in JMRI, so for test, don't assume them to be loaded
117                //node.setFeature(new Feature(BidibLibrary.FEATURE_GEN_SWITCH_ACK, 0)); //Test
118
119                Feature relevantPidBits = Feature.findFeature(node.getFeatures(), BidibLibrary.FEATURE_RELEVANT_PID_BITS);
120                if (relevantPidBits != null) {
121                    node.setRelevantPidBits(relevantPidBits.getValue());
122                }
123                Feature stringSize = Feature.findFeature(node.getFeatures(), BidibLibrary.FEATURE_STRING_SIZE);
124                if (stringSize != null) {
125                    node.setStringSize(stringSize.getValue());
126                }
127                Integer portcount = 0;
128                Feature flatModel = Feature.findFeature(node.getFeatures(), BidibLibrary.FEATURE_CTRL_PORT_FLAT_MODEL_EXTENDED);
129                if (flatModel != null) {
130                    portcount = flatModel.getValue() * 256;
131                }
132                flatModel = Feature.findFeature(node.getFeatures(), BidibLibrary.FEATURE_CTRL_PORT_FLAT_MODEL);
133                if (flatModel != null) {
134                    portcount += flatModel.getValue();
135                }
136                if (portcount > 0) {
137                    node.setPortFlatModel(portcount);
138                }
139            }
140            log.debug("+++ node init finished: {}", node);
141        }
142    }
143    
144    /**
145     * Remove a node from all named beans and from the nodes list
146     * 
147     * @param node to remove
148     */
149    public void nodeLost(Node node) {
150        log.error("BiDiB node lost! {}", node);
151        startNodeUpdate(node, false);
152    }
153    
154    /**
155     * Add a node to nodes list and notify all named beans to update
156     * 
157     * @param node to add
158     */
159    public void nodeNew(Node node) {
160        log.warn("New BiDiB node found {}", node);
161        long uid = node.getUniqueId() & 0x0000ffffffffffL; //mask the classid
162        nodes.put(uid, node);
163        startNodeUpdate(node, true);
164    }
165    
166    /**
167     * Remove all nodes
168     */
169    public void connectionLost() {
170        if (!nodes.isEmpty()) {
171            log.warn("remove all nodes from connection.");
172            for(Map.Entry<Long, Node> entry : nodes.entrySet()) {
173                Node node = entry.getValue();
174                startNodeUpdate(node, false);
175            }
176        }
177    }
178    
179    /**
180     * Get data from root node and from all other nodes
181     * 
182     * @return true on success
183     */
184    
185    public boolean connectionInit() {
186        try {
187            log.debug("get relevant node data");
188            BidibNode rootNode = bidib.getRootNode();
189            int magic = rootNode.getMagic(0);
190            log.debug("Root Node returned magic: 0x{}", ByteUtils.magicToHex(magic));
191            if (magic != 0xAFFE) {
192                return false;
193            }
194            log.trace("root node: {}, node: {}", rootNode, ((RootNode)rootNode).getMasterNode());
195            int count = rootNode.getNodeCount();
196            log.debug("node count: {}", count);
197            byte[] nodeaddr = rootNode.getAddr();
198            log.debug("node addr length: {}", nodeaddr.length);
199            log.debug("node addr: {}", nodeaddr);
200            for (int i = 0; i < nodeaddr.length; i++) {
201                log.debug("  byte {}: {}", i, nodeaddr[i]);
202            }
203    //DEBUG
204    //            int featureCount = rootNode.getFeatureCount();
205    //            log.debug("feature count: {}", featureCount);
206    //            log.debug("** Unique ID: {}", String.format("0x%X",rootNode.getUniqueId()));
207
208            for (int index = 1; index <= count; index++) {
209                Node node = rootNode.getNextNode(null); //TODO org.bidib.jbidibc.messages.logger.Logger
210                initNode(node);
211                long uid = node.getUniqueId() & 0x0000ffffffffffL; //mask the classid
212                nodes.put(uid, node);
213            }
214            rootNode.sysEnable();
215            log.info("--- node init finished ---");
216
217            Node csnode = tc.getFirstCommandStationNode();
218            if (csnode != null) {
219                tc.sendBiDiBMessage(new CommandStationSetStateMessage(CommandStationState.QUERY), csnode);
220                // TODO: Should we remove all Locos from command station? MSG_SET_DRIVE with loco 0 and bitfields = 0 (see BiDiB spec)
221                // TODO: use MSG_CS_ALLOCATE every second to disable direct control from local controllers like handhelds?
222            }
223
224            return true;
225        }
226        catch (ProtocolException ex) {
227            log.error("The connection was not unavailable: {}. Verify that the BiDiB device is connected.", ex.getMessage());
228        }
229//        catch (Exception ex) {
230//            log.error("Execute command failed: ", ex); // NOSONAR
231//        }
232        return false;
233        
234    }
235    
236    
237    // private methods to execute nodeLost/nodeNew in a low priority thread
238        
239    private <T> void nodeLost(SortedSet<T> beanSet, long uniqueId) {
240        beanSet.forEach( (nb) -> {
241            if (nb instanceof BiDiBNamedBeanInterface) {
242                BiDiBAddress addr = ((BiDiBNamedBeanInterface)nb).getAddr();
243                //log.trace("check bean: {}", nb);
244                if (addr.getNodeUID() == uniqueId) {
245                    log.trace("-invalidate {}", nb);
246                    addr.invalidate();
247                    ((BiDiBNamedBeanInterface)nb).nodeLost();
248                }
249            }
250        });
251    }
252    
253    private void nodeLostBeans(long uniqueId) {
254        long uid = uniqueId & 0x0000ffffffffffL; //mask the classid
255        nodeLost(InstanceManager.getDefault(jmri.TurnoutManager.class).getNamedBeanSet(), uid);
256        nodeLost(InstanceManager.getDefault(jmri.SensorManager.class).getNamedBeanSet(), uid);
257        nodeLost(InstanceManager.getDefault(jmri.LightManager.class).getNamedBeanSet(), uid);
258        nodeLost(InstanceManager.getDefault(jmri.ReporterManager.class).getNamedBeanSet(), uid);
259        nodeLost(InstanceManager.getDefault(jmri.SignalMastManager.class).getNamedBeanSet(), uid);
260        nodes.remove(uid);
261    }
262    
263    private <T> void nodeNew(SortedSet<T> beanSet, Node node) {
264        beanSet.forEach( (nb) -> {
265            if (nb instanceof BiDiBNamedBeanInterface) {
266                BiDiBAddress addr = ((BiDiBNamedBeanInterface)nb).getAddr();
267                //log.trace("check bean: {}", nb);
268                if (!addr.isValid()) {
269                    log.trace("+new bean {}", nb);
270                    ((BiDiBNamedBeanInterface)nb).nodeNew();
271                }
272            }
273        });
274    }
275    
276    private void nodeNewBeans(Node node) {
277        try {
278            tc.getBidib().getRootNode().sysEnable();
279        }
280        catch (ProtocolException e) {
281            log.warn("failed to ENABLE node {}", node, e);
282        }
283        nodeNew(InstanceManager.getDefault(jmri.TurnoutManager.class).getNamedBeanSet(), currentNode.node);
284        nodeNew(InstanceManager.getDefault(jmri.SensorManager.class).getNamedBeanSet(), currentNode.node);
285        nodeNew(InstanceManager.getDefault(jmri.LightManager.class).getNamedBeanSet(), currentNode.node);
286        nodeNew(InstanceManager.getDefault(jmri.ReporterManager.class).getNamedBeanSet(), currentNode.node);
287        nodeNew(InstanceManager.getDefault(jmri.SignalMastManager.class).getNamedBeanSet(), currentNode.node);
288        BiDiBSensorManager bs = (BiDiBSensorManager)tc.getSystemConnectionMemo().getSensorManager();
289        if (bs != null) {
290            bs.updateNodeFeedbacks(node);
291        }
292        BiDiBReporterManager br = (BiDiBReporterManager)tc.getSystemConnectionMemo().getReporterManager();
293        if (br != null) {
294            br.updateNode(node);
295        }
296    }
297    
298    /**
299     * Insert a node into the queue. Start Thread if currently not running
300     * 
301     * @param node to add or remove
302     * @param isNewNode - true: add new node, false: remove node
303     */
304    private void startNodeUpdate(Node node, boolean isNewNode) {
305        
306        synchronized (queue) {
307            // check if the thread is still working 
308            if (queue.isEmpty()  ||  initThread == null  ||  !initThread.isAlive()) {
309                if (initThread != null) {
310                    try {
311                        initThread.join(1000); //wait until the thread has definitly died
312                    }
313                    catch (InterruptedException e) {}
314                }
315                initThread = new Thread(this, "NodeInitThread"); //create a new thread
316                initThread.setPriority(Thread.MIN_PRIORITY);
317                queue.add(new SimplePair(node, isNewNode));
318                initThread.start();
319                log.debug("thread was started - return");
320            }
321            else {
322                // Thread running, just add the node to the queue
323                queue.add(new SimplePair(node, isNewNode));
324                log.debug("thread running, just add node to queue and return");
325            }
326        }
327    }
328    
329    
330    /**
331     * Execute queued node init and named beans update.
332     * Finish the thread if the queue is empty
333     */
334    @Override
335    public void run() {
336        log.debug("starting thread for node initialization");
337        while (true) {
338            log.trace("-- loop, queue size: {}", queue.size());
339            synchronized (queue) {
340                log.trace("  currentNode: {}", currentNode);
341                if (currentNode != null) { //if we just processed a node ...
342                    queue.removeFirst(); //...remove it from the queue
343                    currentNode = null;
344                }
345                currentNode = queue.peekFirst(); //get next from queue
346                if (currentNode == null) {
347                    break; //exit while loop and stop thread by exiting run()
348                }
349            }
350            // now do the real work - initialize node and beans
351            if (currentNode.isNewNode) {
352                try {
353                    initNode(currentNode.node);
354                    nodeNewBeans(currentNode.node);
355                }
356                catch (Exception e) {
357                    log.warn("error initializing node {}", currentNode.node, e);
358                }
359            }
360            else {
361                nodeLostBeans(currentNode.node.getUniqueId());
362            }
363        }
364        log.debug("thread finished for node");
365    }
366
367    private final static Logger log = LoggerFactory.getLogger(BiDiBNodeInitializer.class);
368}