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.messages.BidibLibrary;
012import org.bidib.jbidibc.messages.Feature;
013import org.bidib.jbidibc.messages.FeatureData;
014import org.bidib.jbidibc.messages.Node;
015import org.bidib.jbidibc.messages.StringData;
016import org.bidib.jbidibc.messages.exception.ProtocolException;
017import org.bidib.jbidibc.messages.utils.ByteUtils;
018
019import org.slf4j.Logger;
020import org.slf4j.LoggerFactory;
021
022/**
023 * This class initializes or deinitializes a BiDiB node when it is found on system startup or if it
024 * is discovered or lost while the system is running.\
025 *
026 * The real work is done in its own thread and from a node queue. Initializing is a time consuming
027 * process since a lot of data is read from the node.
028 * 
029 * @author Eckart Meyer Copyright (C) 2023
030 */
031public class BiDiBNodeInitializer implements Runnable {
032
033    private static class SimplePair {
034        public Node node;
035        public boolean isNewNode; //true: new node, false: node lost
036        
037        public SimplePair(Node node, boolean isNewNode) {
038            this.node = node;
039            this.isNewNode = isNewNode;
040        }
041    }
042    
043    private final Map<Long, Node> nodes;
044    private final BidibInterface bidib;
045    private final BiDiBTrafficController tc;
046    private SimplePair currentNode;
047    private Thread initThread;
048    private final LinkedList<SimplePair> queue;
049    
050    
051    public BiDiBNodeInitializer(BiDiBTrafficController tc, BidibInterface bidib, Map<Long, Node> nodes) {
052        this.bidib = bidib;
053        this.nodes = nodes;
054        this.tc = tc;
055        queue = new LinkedList<>();
056        log.debug("BiDiB node initializer created");
057    }
058    
059    /**
060     * Get everything we need from the node. The node must already be inserted into the BiDiB node list.
061     * 
062     * @param node node to initialize
063     * @throws ProtocolException when features can't be loaded
064     */
065    public void initNode(Node node) throws ProtocolException {
066        if (node != null) {
067            BidibNode bidibNode = bidib.getNode(node);
068            log.info("+++ found node: {}", node);
069
070            int magic = bidibNode.getMagic(0);
071            log.debug("Node returned magic: 0x{}", ByteUtils.magicToHex(magic));
072            if (magic == 0xAFFE) {
073                node.setStoredString(StringData.INDEX_PRODUCTNAME, bidibNode.getString(0, StringData.INDEX_PRODUCTNAME).getValue());
074                node.setStoredString(StringData.INDEX_USERNAME, bidibNode.getString(StringData.NAMESPACE_NODE, StringData.INDEX_USERNAME).getValue());
075                node.setProtocolVersion(bidibNode.getProtocolVersion());
076                node.setSoftwareVersion(bidibNode.getSwVersion());
077                log.info("Product name: {}", node.getStoredString(StringData.INDEX_PRODUCTNAME));
078                log.info("User name: {}", node.getStoredString(StringData.INDEX_USERNAME));
079                log.info("Protocol version: {}", node.getProtocolVersion());
080                log.info("Software version: {}", node.getSoftwareVersion());
081
082                try {
083                    FeatureData features = bidibNode.getFeaturesAll();
084                    log.info("featureCount: {}", features.getFeatureCount());
085                    if (features.isStreamingSupport()) {
086                        int k = 1;//counter is for debug only
087                        for (Feature feature : features.getFeatures()) {
088                            log.trace("feature #{}/{}", k++, features.getFeatureCount());
089                            log.info("feature.type: {}, value: {}, name: {}", feature.getType(), feature.getValue(), feature.getFeatureName());
090                            node.setFeature(feature);
091                        }
092                    }
093                    else {
094                        Feature feature;
095                        int k = 1;//counter is for debug only
096                        try {
097                            while ((feature = bidibNode.getNextFeature()) != null) {
098                                log.trace("feature #{}/{}", k++, features.getFeatureCount());
099                                log.info("feature.type: {}, value: {}, name: {}", feature.getType(), feature.getValue(), feature.getFeatureName());
100                                node.setFeature(feature);
101                            }
102                        }
103                        catch (ProtocolException ex) {
104                            log.debug("No more features.");
105                        }
106                    }
107                }
108                catch (ProtocolException ex) {
109                    log.error("Features can't be loaded from node: {}", ex.getMessage());
110                }
111                log.info("Finished query features."); // NOSONAR
112
113                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
114                //node.setFeature(new Feature(BidibLibrary.FEATURE_GEN_SWITCH_ACK, 0)); //Test
115
116                Feature relevantPidBits = Feature.findFeature(node.getFeatures(), BidibLibrary.FEATURE_RELEVANT_PID_BITS);
117                if (relevantPidBits != null) {
118                    node.setRelevantPidBits(relevantPidBits.getValue());
119                }
120                Feature stringSize = Feature.findFeature(node.getFeatures(), BidibLibrary.FEATURE_STRING_SIZE);
121                if (stringSize != null) {
122                    node.setStringSize(stringSize.getValue());
123                }
124                Integer portcount = 0;
125                Feature flatModel = Feature.findFeature(node.getFeatures(), BidibLibrary.FEATURE_CTRL_PORT_FLAT_MODEL_EXTENDED);
126                if (flatModel != null) {
127                    portcount = flatModel.getValue() * 256;
128                }
129                flatModel = Feature.findFeature(node.getFeatures(), BidibLibrary.FEATURE_CTRL_PORT_FLAT_MODEL);
130                if (flatModel != null) {
131                    portcount += flatModel.getValue();
132                }
133                if (portcount > 0) {
134                    node.setPortFlatModel(portcount);
135                }
136            }
137            log.debug("+++ node init finished: {}", node);
138        }
139    }
140    
141    /**
142     * Remove a node from all named beans and from the nodes list
143     * 
144     * @param node to remove
145     */
146    public void nodeLost(Node node) {
147        log.error("BiDiB node lost! {}", node);
148        startNodeUpdate(node, false);
149    }
150    
151    /**
152     * Add a node to nodes list and notify all named beans to update
153     * 
154     * @param node to add
155     */
156    public void nodeNew(Node node) {
157        log.warn("New BiDiB node found {}", node);
158        long uid = node.getUniqueId() & 0x0000ffffffffffL; //mask the classid
159        nodes.put(uid, node);
160        startNodeUpdate(node, true);
161    }
162    
163    
164    // private methods to execute nodeLost/nodeNew in one low priority thread
165        
166    private <T> void nodeLost(SortedSet<T> beanSet, long uniqueId) {
167        beanSet.forEach( (nb) -> {
168            if (nb instanceof BiDiBNamedBeanInterface) {
169                BiDiBAddress addr = ((BiDiBNamedBeanInterface)nb).getAddr();
170                log.trace("check bean: {}", nb);
171                if (addr.getNodeUID() == uniqueId) {
172                    addr.invalidate();
173                    ((BiDiBNamedBeanInterface)nb).nodeLost();
174                }
175            }
176        });
177    }
178    
179    private void nodeLostBeans(long uniqueId) {
180        long uid = uniqueId & 0x0000ffffffffffL; //mask the classid
181        nodeLost(InstanceManager.getDefault(jmri.TurnoutManager.class).getNamedBeanSet(), uid);
182        nodeLost(InstanceManager.getDefault(jmri.SensorManager.class).getNamedBeanSet(), uid);
183        nodeLost(InstanceManager.getDefault(jmri.LightManager.class).getNamedBeanSet(), uid);
184        nodeLost(InstanceManager.getDefault(jmri.ReporterManager.class).getNamedBeanSet(), uid);
185        nodeLost(InstanceManager.getDefault(jmri.SignalMastManager.class).getNamedBeanSet(), uid);
186        nodes.remove(uid);
187    }
188    
189    private <T> void nodeNew(SortedSet<T> beanSet, Node node) {
190        beanSet.forEach( (nb) -> {
191            if (nb instanceof BiDiBNamedBeanInterface) {
192                BiDiBAddress addr = ((BiDiBNamedBeanInterface)nb).getAddr();
193                log.trace("check bean: {}", nb);
194                if (!addr.isValid()) {
195                    ((BiDiBNamedBeanInterface)nb).nodeNew();
196                }
197            }
198        });
199    }
200    
201    private void nodeNewBeans(Node node) {
202        try {
203            tc.getBidib().getRootNode().sysEnable();
204        }
205        catch (ProtocolException e) {
206            log.warn("failed to ENABLE node {}", node, e);
207        }
208        nodeNew(InstanceManager.getDefault(jmri.TurnoutManager.class).getNamedBeanSet(), currentNode.node);
209        nodeNew(InstanceManager.getDefault(jmri.SensorManager.class).getNamedBeanSet(), currentNode.node);
210        nodeNew(InstanceManager.getDefault(jmri.LightManager.class).getNamedBeanSet(), currentNode.node);
211        nodeNew(InstanceManager.getDefault(jmri.ReporterManager.class).getNamedBeanSet(), currentNode.node);
212        nodeNew(InstanceManager.getDefault(jmri.SignalMastManager.class).getNamedBeanSet(), currentNode.node);
213        BiDiBSensorManager bs = (BiDiBSensorManager)tc.getSystemConnectionMemo().getSensorManager();
214        if (bs != null) {
215            bs.updateNodeFeedbacks(node);
216        }
217        BiDiBReporterManager br = (BiDiBReporterManager)tc.getSystemConnectionMemo().getReporterManager();
218        if (br != null) {
219            br.updateNode(node);
220        }
221    }
222    
223    /**
224     * Insert a node into the queue. Start Thread if currently not running
225     * 
226     * @param node to add or remove
227     * @param isNewNode - true: add new node, false: remove node
228     */
229    private void startNodeUpdate(Node node, boolean isNewNode) {
230        
231        synchronized (queue) {
232            // check if the thread is still working 
233            if (queue.isEmpty()  ||  initThread == null  ||  !initThread.isAlive()) {
234                if (initThread != null) {
235                    try {
236                        initThread.join(1000); //wait until the thread has definitly died
237                    }
238                    catch (InterruptedException e) {}
239                }
240                initThread = new Thread(this, "NodeInitThread"); //create a new thread
241                initThread.setPriority(Thread.MIN_PRIORITY);
242                queue.add(new SimplePair(node, isNewNode));
243                initThread.start();
244                log.debug("thread was started - return");
245            }
246            else {
247                // Thread running, just add the node to the queue
248                queue.add(new SimplePair(node, isNewNode));
249                log.debug("thread running, just add node to queue and return");
250            }
251        }
252    }
253    
254    
255    /**
256     * Execute queued node init and named beans update.
257     * Finish the thread if the queue is empty
258     */
259    @Override
260    public void run() {
261        log.debug("starting thread for node initialization");
262        while (true) {
263            log.trace("-- loop, queue size: {}", queue.size());
264            synchronized (queue) {
265                log.trace("  currentNode: {}", currentNode);
266                if (currentNode != null) { //if we just processed a node ...
267                    queue.removeFirst(); //...remove it from the queue
268                    currentNode = null;
269                }
270                currentNode = queue.peekFirst(); //get next from queue
271                if (currentNode == null) {
272                    break; //exit while loop and stop thread by exiting run()
273                }
274            }
275            // now do the real work - initialize node and beans
276            if (currentNode.isNewNode) {
277                try {
278                    initNode(currentNode.node);
279                    nodeNewBeans(currentNode.node);
280                }
281                catch (Exception e) {
282                    log.warn("error initializing node {}", currentNode.node, e);
283                }
284            }
285            else {
286                nodeLostBeans(currentNode.node.getUniqueId());
287            }
288        }
289        log.debug("thread finished for node");
290    }
291
292    private final static Logger log = LoggerFactory.getLogger(BiDiBNodeInitializer.class);
293}