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}