001package jmri.jmrix.bidib; 002 003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 004 005import java.util.HashMap; 006import java.util.TreeMap; 007import java.util.LinkedHashSet; 008import java.util.List; 009import java.util.Map; 010import java.util.Set; 011import java.util.Collection; 012import java.util.concurrent.CountDownLatch; 013import java.util.concurrent.TimeUnit; 014import java.util.concurrent.atomic.AtomicBoolean; 015 016import java.awt.event.ActionListener; 017import java.awt.event.ActionEvent; 018 019import jmri.CommandStation; 020import jmri.jmrix.PortAdapter; 021import jmri.NmraPacket; 022import jmri.InstanceManager; 023import jmri.ShutDownManager; 024import jmri.ShutDownTask; 025import jmri.implementation.AbstractShutDownTask; 026import jmri.jmrix.bidib.netbidib.NetBiDiBPairingRequestDialog; 027 028import org.bidib.jbidibc.messages.BidibLibrary; 029import org.bidib.jbidibc.messages.exception.ProtocolException; 030import org.bidib.jbidibc.messages.utils.ByteUtils; 031 032import org.bidib.jbidibc.core.BidibMessageProcessor; 033import org.bidib.jbidibc.core.BidibInterface; 034import org.bidib.jbidibc.messages.BidibPort; 035import org.bidib.jbidibc.messages.ConnectionListener; 036import org.bidib.jbidibc.core.DefaultMessageListener; 037import org.bidib.jbidibc.messages.Feature; 038import org.bidib.jbidibc.messages.LcConfig; 039import org.bidib.jbidibc.messages.LcConfigX; 040import org.bidib.jbidibc.core.MessageListener; 041import org.bidib.jbidibc.messages.base.RawMessageListener; 042import org.bidib.jbidibc.messages.Node; 043import org.bidib.jbidibc.core.NodeListener; 044import org.bidib.jbidibc.messages.ProtocolVersion; 045import org.bidib.jbidibc.messages.StringData; 046import org.bidib.jbidibc.messages.helpers.Context; 047import org.bidib.jbidibc.core.node.listener.TransferListener; 048import org.bidib.jbidibc.messages.exception.PortNotFoundException; 049import org.bidib.jbidibc.messages.message.BidibCommandMessage; 050import org.bidib.jbidibc.core.node.BidibNodeAccessor; 051import org.bidib.jbidibc.messages.utils.NodeUtils; 052import org.bidib.jbidibc.messages.enums.CommandStationState; 053import org.bidib.jbidibc.messages.enums.LcOutputType; 054import org.bidib.jbidibc.messages.enums.PortModelEnum; 055import org.bidib.jbidibc.messages.message.AccessoryGetMessage; 056import org.bidib.jbidibc.messages.message.BidibRequestFactory; 057import org.bidib.jbidibc.messages.message.CommandStationSetStateMessage; 058import org.bidib.jbidibc.messages.message.FeedbackGetRangeMessage; 059import org.bidib.jbidibc.messages.message.LocalPingMessage; 060import org.bidib.jbidibc.core.node.CommandStationNode; 061import org.bidib.jbidibc.core.node.BoosterNode; 062import org.bidib.jbidibc.core.node.RootNode; 063import org.bidib.jbidibc.messages.BoosterStateData; 064import org.bidib.jbidibc.messages.enums.BoosterControl; 065import org.bidib.jbidibc.messages.enums.BoosterState; 066import org.bidib.jbidibc.messages.enums.CommandStationProgState; 067import org.bidib.jbidibc.messages.enums.PairingResult; 068import org.bidib.jbidibc.messages.message.netbidib.NetBidibLinkData; 069import org.bidib.jbidibc.messages.port.BytePortConfigValue; 070import org.bidib.jbidibc.messages.port.PortConfigValue; 071import org.bidib.jbidibc.messages.port.ReconfigPortConfigValue; 072import org.bidib.jbidibc.messages.utils.CollectionUtils; 073import org.bidib.jbidibc.netbidib.NetBidibContextKeys; 074import org.bidib.jbidibc.netbidib.client.pairingstates.PairingStateEnum; 075import org.bidib.jbidibc.simulation.comm.SimulationBidib; 076 077import org.slf4j.Logger; 078import org.slf4j.LoggerFactory; 079 080/** 081 * The BiDiB Traffic Controller provides the interface for JMRI to the BiDiB Library (jbidibc) - it 082 * does not handle any protocol functions itself. Therefor it does not extend AbstractMRTrafficController. 083 * Instead, it delegates BiDiB handling to a BiDiB controller instance (serial, simulation, etc.) using BiDiBInterface. 084 * 085 * @author Bob Jacobsen Copyright (C) 2002 086 * @author Eckart Meyer Copyright (C) 2019-2025 087 * 088 */ 089 090@SuppressFBWarnings(value = "JLM_JSR166_UTILCONCURRENT_MONITORENTER") 091// This code uses several AtomicBoolean variables as synch objects. In this use, 092// they're synchronizing access to code blocks, not just synchronizing access 093// to the underlying boolean value. it would be possible to use separate 094// Object variables for this process, but keeping those in synch would actually 095// be more complex and confusing than this approach. 096 097public class BiDiBTrafficController implements CommandStation { 098 099 public static final String ASYNCCONNECTIONINIT = "jmri-async-init"; 100 public static final String ISNETBIDIB = "jmri-is-netbidib"; 101 public static final String USELOCALPING = "jmri-use-local-ping"; 102 103 private final BidibInterface bidib; 104 private final Set<TransferListener> transferListeners = new LinkedHashSet<>(); 105 private final Set<MessageListener> messageListeners = new LinkedHashSet<>(); 106 private final Set<NodeListener> nodeListeners = new LinkedHashSet<>(); 107 private final AtomicBoolean stallLock = new AtomicBoolean(); 108 private java.util.TimerTask watchdogTimer = null; 109 private final AtomicBoolean watchdogStatus = new AtomicBoolean(); 110 private final CountDownLatch continueLock = new CountDownLatch(1); //wait for all initialisations are done 111 private NetBiDiBPairingRequestDialog pairingDialog = null; 112 private PairingResult curPairingResult = PairingResult.UNPAIRED; 113 private boolean isAsyncInit = false; 114 private boolean isNetBiDiB = false; 115 private boolean useLocalPing = true; 116 private boolean connectionIsReady = false; 117 private final BiDiBNodeInitializer nodeInitializer; 118 private BiDiBPortController portController; 119 private final Set<ActionListener> connectionChangedListeners = new LinkedHashSet<>(); 120// private Long deviceUniqueId = null; //this is the unique Id of the device (root node) 121 protected final TreeMap<Long, Node> nodes = new TreeMap<>(); //our node list - use TreeMap since it retains order if insertion (HashMap has arbitrary order) 122 123 private Node cachedCommandStationNode = null; 124 125 private final AtomicBoolean mIsProgMode = new AtomicBoolean(); 126 volatile protected CommandStationState mSavedMode; 127 private Node currentGlobalProgrammerNode = null; 128 private final javax.swing.Timer progTimer = new javax.swing.Timer(200, e -> progTimeout()); 129 private final javax.swing.Timer localPingTimer = new javax.swing.Timer(4000, e -> localPingTimeout()); 130 private final javax.swing.Timer delayedCloseTimer; 131 132 private final Map<Long, String> debugStringBuffer = new HashMap<>(); 133 134 /** 135 * Create a new BiDiBTrafficController instance. 136 * Must provide a BidibInterface reference at creation time. 137 * 138 * @param b reference to associated jbidibc object, 139 * preserved for later. 140 */ 141 public BiDiBTrafficController(BidibInterface b) { 142 bidib = b; 143 delayedCloseTimer = new javax.swing.Timer(1000, e -> bidib.close() ); 144 delayedCloseTimer.setRepeats(false); 145 146 log.debug("BiDiBTrafficController created"); 147 mSavedMode = CommandStationState.OFF; 148 setWatchdogTimer(false); //preset not enabled 149 150 progTimer.setRepeats(false); 151 mIsProgMode.set(false); 152 153 nodeInitializer = new BiDiBNodeInitializer(this, bidib, nodes); 154 155 } 156 157 /** 158 * Opens the BiDiB connection in the jbidibc library, add listeners and initialize BiDiB. 159 * 160 * @param p BiDiB port adapter (serial or simulation) 161 * @return a jbidibc context 162 */ 163 @SuppressFBWarnings(value = "BC_UNCONFIRMED_CAST",justification = "Cast safe by design") 164 public Context connnectPort(PortAdapter p) { 165 // init bidib 166 portController = (BiDiBPortController)p; 167 Context context = portController.getContext(); 168 169 // there is currently no difference between "use async init" and "this is a netBiDiB device"... 170 isAsyncInit = context.get(ASYNCCONNECTIONINIT, Boolean.class, false); 171 isNetBiDiB = context.get(ISNETBIDIB, Boolean.class, false); 172 useLocalPing = context.get(USELOCALPING, Boolean.class, true); 173 174 stallLock.set(false); //not stalled 175 176 messageListeners.add(new DefaultMessageListener() { 177 178 @Override 179 public void error(byte[] address, int messageNum, int errorCode, byte[] reasonData) { 180 log.debug("Node error event: addr: {}, msg num: {}, error code: {}, data: {}", address, messageNum, errorCode, reasonData); 181 if (errorCode == 1) { 182 log.info("error: {}", new String(reasonData)); 183 } 184 } 185 186 @Override 187 @SuppressFBWarnings(value = "SLF4J_SIGN_ONLY_FORMAT",justification = "info message contains context information") 188 public void nodeString(byte[] address, int messageNum, int namespace, int stringId, String value) { 189 // handle debug messages from a node 190 if (namespace == StringData.NAMESPACE_DEBUG) { 191 Node node = getNodeByAddr(address); 192 String uid = ByteUtils.getUniqueIdAsString(node.getUniqueId()); 193 // the debug string buffer key is the node's 40 bit UID plus the string id in the upper 24 bit 194 long key = (node.getUniqueId() & 0x0000ffffffffffL) | (long)stringId << 40; 195 String prefix = "===== BiDiB"; 196 if (value.charAt(value.length() - 1) == '\n') { 197 String txt = ""; 198 // check if we have previous received imcomplete text 199 if (debugStringBuffer.containsKey(key)) { 200 txt = debugStringBuffer.get(key); 201 debugStringBuffer.remove(key); 202 } 203 txt += value.replace("\n",""); 204 switch(stringId) { 205 case StringData.INDEX_DEBUG_STDOUT: 206 log.info("{} {} stdout: {}", prefix, uid, txt); 207 break; 208 case StringData.INDEX_DEBUG_STDERR: 209 log.info("{} {} stderr: {}", prefix, uid, txt); 210 break; 211 case StringData.INDEX_DEBUG_WARN: 212 log.warn("{} {}: {}", prefix, uid, txt); 213 break; 214 case StringData.INDEX_DEBUG_INFO: 215 log.info("{} {}: {}", prefix, uid, txt); 216 break; 217 case StringData.INDEX_DEBUG_DEBUG: 218 log.debug("{} {}: {}", prefix, uid, txt); 219 break; 220 case StringData.INDEX_DEBUG_TRACE: 221 log.trace("{} {}: {}", prefix, uid, txt); 222 break; 223 default: break; 224 } 225 } 226 else { 227 log.trace("incomplete debug string received: [{}]", value); 228 String txt = ""; 229 if (debugStringBuffer.containsKey(key)) { 230 txt = debugStringBuffer.get(key); 231 } 232 debugStringBuffer.put(key, (txt + value)); 233 } 234 } 235 } 236 237 @Override 238 public void nodeLost(byte[] address, int messageNum, Node node) { 239 log.debug("Node lost event: {}", node); 240 nodeInitializer.nodeLost(node); 241 } 242 243 @Override 244 public void nodeNew(byte[] address, int messageNum, Node node) { 245 log.debug("Node new event: {}", node); 246 nodeInitializer.nodeNew(node); 247 } 248 249 @Override 250 public void stall(byte[] address, int messageNum, boolean stall) { 251 log.trace("message listener stall! {} node: {}", stall, getNodeByAddr(address)); 252 synchronized (stallLock) { 253 if (log.isDebugEnabled()) { 254 Node node = getNodeByAddr(address); 255 log.debug("stall - msg num: {}, new state: {}, node: {}, ", messageNum, stall, node); 256 } 257 if (stall != stallLock.get()) { 258 stallLock.set(stall); 259 if (!stall) { 260 log.debug("stall - wake send"); 261 stallLock.notifyAll(); //wake pending send if any 262 } 263 } 264 } 265 } 266 267 @Override 268 public void localPong(byte[] address, int messageNum) { 269 if (log.isTraceEnabled()) { 270 Node node = getNodeByAddr(address); 271 log.trace("local pong - msg num: {}, node: {}, ", messageNum, node); 272 } 273 } 274 275 // don't know if this is the correct place... 276 @Override 277 public void csState(byte[] address, int messageNum, CommandStationState commandStationState) { 278 Node node = getNodeByAddr(address); 279 log.debug("CS STATE event: {} on node {}, current watchdog status: {}", commandStationState, node, watchdogStatus.get()); 280 synchronized (mIsProgMode) { 281 if (CommandStationState.isPtProgState(commandStationState)) { 282 mIsProgMode.set(true); 283 } 284 else { 285 mIsProgMode.set(false); 286 mSavedMode = commandStationState; 287 } 288 } 289 boolean newState = (commandStationState == CommandStationState.GO); 290 if (node == getFirstCommandStationNode() && newState != watchdogStatus.get()) { 291 log.trace("watchdog: new state: {}, current state: {}", newState, watchdogStatus.get()); 292 setWatchdogTimer(newState); 293 } 294 } 295 @Override 296 public void csProgState( 297 byte[] address, int messageNum, CommandStationProgState commandStationProgState, int remainingTime, int cvNumber, int cvData) { 298 synchronized (progTimer) { 299 if ( (commandStationProgState.getType() & 0x80) != 0) { //bit 7 = 1 means operation has finished 300 progTimer.restart(); 301 log.trace("PROG finished, progTimer (re)started."); 302 } 303 else { 304 progTimer.stop(); 305 log.trace("PROG pending, progTimer stopped."); 306 } 307 } 308 } 309 @Override 310 public void boosterState(byte[] address, int messageNum, BoosterState state, BoosterControl control) { 311 Node node = getNodeByAddr(address); 312 log.info("BOOSTER STATE & CONTROL was signalled: {}, control: {}", state.getType(), control.getType()); 313 if (node != getFirstCommandStationNode() && node == currentGlobalProgrammerNode && control != BoosterControl.LOCAL) { 314 currentGlobalProgrammerNode = null; 315 } 316 } 317 }); 318 319 transferListeners.add(new TransferListener() { 320 321 @Override 322 public void sendStopped() { 323 // no implementation 324 //log.trace("sendStopped"); 325 } 326 327 @Override 328 public void sendStarted() { 329 log.debug("sendStarted"); 330 // TODO check node! 331 synchronized (stallLock) { 332 if (stallLock.get()) { 333 try { 334 log.debug("sendStarted is stalled - waiting..."); 335 stallLock.wait(1000L); 336 log.debug("sendStarted stall condition has been released"); 337 } 338 catch (InterruptedException e) { 339 log.warn("waited too long for releasing stall condition - continue..."); 340 stallLock.set(false); 341 } 342 } 343 } 344 } 345 346 @Override 347 public void receiveStopped() { 348 // no implementation 349 //log.trace("receiveStopped"); 350 } 351 352 @Override 353 public void receiveStarted() { 354 // no implementation 355 //log.trace("receiveStarted"); 356 } 357 358 @Override 359 public void ctsChanged(boolean cts, boolean manualEvent) { //new 360// public void ctsChanged(boolean cts) { //jbidibc 12.5 361 // no implementation 362 log.trace("ctsChanged"); 363 } 364 }); 365 366 ConnectionListener connectionListener = new ConnectionListener() { 367 368 /** 369 * The port was opened. 370 * In case of a netBiDiB connection pairing has not been verified and the 371 * server is not logged in. This will be notified later. 372 * 373 * For all other connections the port is now usable and we init the 374 * connection just here. 375 * 376 * @param port is the name of the port just opened. 377 */ 378 @Override 379 public void opened(String port) { 380 log.debug("opened port {}", port); 381 if (!isAsyncInit) { 382 connectionIsReady = true; 383 nodeInitializer.connectionInit(); 384 continueLock.countDown(); 385 } 386 } 387 388 /** 389 * The port was closed 390 * If the local ping feature was enabled (netBiDiB), it stopped now. 391 * 392 * @param port is the name of the closed port 393 */ 394 @Override 395 public void closed(String port) { 396 log.debug("closed port {}", port); 397 if (bidib.getRootNode() != null) { 398 if (bidib.getRootNode().isLocalPingEnabled()) { 399 log.debug("Stop local ping"); 400 bidib.getRootNode().stopLocalPingWorker(); 401 } 402 } 403 connectionIsReady = false; 404 nodeInitializer.connectionLost(); 405 continueLock.countDown(); 406 fireConnectionChanged("closed"); 407 } 408 409 @Override 410 public void stall(boolean stall) { 411 // no implementation 412 log.info("connection stall! {}", stall); 413 } 414 415 @Override 416 public void status(String messageKey, Context context) { 417 // no implementation 418 log.trace("status - message key {}", messageKey); 419 } 420 421 /** 422 * This is called if the pairing state machine is now either in "paired" 423 * or "unpaired" state. 424 * 425 * If "paired" is signalled, there is nothing more to do since the 426 * connection will only be ready after server has logged in. 427 * 428 * If pairing failed, timed out or the an existing pairing was removed 429 * on the remote side, we will clear all nodes and then close the connection. 430 * 431 * @param pairingResult 432 */ 433 @Override 434 public void pairingFinished(final PairingResult pairingResult, long uniqueId) { 435 log.debug("** pairingFinished - result: {}, uniqueId: {}", pairingResult, 436 ByteUtils.convertUniqueIdToString(ByteUtils.convertLongToUniqueId(uniqueId))); 437 curPairingResult = pairingResult; 438 //deviceUniqueId = uniqueId; 439 if (!curPairingResult.equals(PairingResult.PAIRED)) { 440 // The pairing timed out or was cancelled on the server side. 441 // Cancelling is also possible while in normal operation. 442 // Close the connection. 443 log.warn("Unpaired!"); 444 connectionIsReady = false; 445 nodeInitializer.connectionLost(); 446 if (continueLock.getCount() == 0 && bidib.isOpened()) { 447 //bidib.close(); //close() from a listener causes an exception in jbibibc, so delay the close 448 delayedCloseTimer.start(); 449 } 450 continueLock.countDown(); 451 } 452 } 453 454 /** 455 * Called if the connection was not paired before (either local or remote). 456 * Send a pairing request to the remote an display a dialog box to inform 457 * the user to acknoledge the pairing at the remote side. The result is notified 458 * to the pairingFinished() event. If the user presses the Cancel button or closes 459 * the window, the connection will be closed (in the mainline code after unlock). 460 */ 461 @Override 462 public void actionRequired(String messageKey, final Context context) { 463 log.info("actionRequired - messageKey: {}, context: {}", messageKey, context); 464 if (messageKey.equals(NetBidibContextKeys.KEY_ACTION_PAIRING_STATE)) { 465 if (context.get(NetBidibContextKeys.KEY_PAIRING_STATE) == PairingStateEnum.Unpaired) { 466 log.debug("**** send pairing request ****"); 467 log.trace("context: {}", context); 468 // Send a pairing request to the remote side and show a dialog so the user 469 // will be informed. 470 bidib.signalUserAction(NetBidibContextKeys.KEY_PAIRING_REQUEST, context); 471 472 pairingDialog = new NetBiDiBPairingRequestDialog(context, portController, new ActionListener() { 473 474 /** 475 * called when the pairing dialog was closed by the user or if the user pressed the cancel-button. 476 * In this case the init should fail. 477 */ 478 @Override 479 public void actionPerformed(ActionEvent ae) { 480 log.debug("pairingDialog cancelled: {}", ae); 481 curPairingResult = PairingResult.UNPAIRED; //just to be sure... 482 continueLock.countDown(); 483 } 484 }); 485 // Show the dialog. 486 // show() will not wait for user interaction or timeout, but will return immediately 487 pairingDialog.show(); 488 } 489 } 490 } 491 492 /** 493 * The remote side has logged in. The connection is now fully usable. 494 */ 495 @Override 496 public void logonReceived(int localNodeAddr, long uniqueId) { 497 log.debug("+++++ logonReceived - localNodeAddr: {}, uniqueId: {}", 498 localNodeAddr, ByteUtils.convertUniqueIdToString(ByteUtils.convertLongToUniqueId(uniqueId))); 499 connectionIsReady = true; 500 //deviceUniqueId = uniqueId; 501 if (bidib.getRootNode().getMasterNode().isDetached()) { 502 bidib.getRootNode().getMasterNode().setDetached(false); 503 } 504 nodeInitializer.connectionInit(); 505 continueLock.countDown(); 506 startLocalPing(); 507 fireConnectionChanged("logon"); 508 } 509 510 /** 511 * The remote side has logged off. Either as an result of a detach from our side or directly 512 * from the remote side. 513 * The nodes of this connection will be removed, but the connection will be not be closed, so 514 * it can be re-established later by a logonReceived event. 515 */ 516 @Override 517 public void logoffReceived(long uniqueId) { 518 log.debug("----- logoffReceived - uniqueId: {}", 519 ByteUtils.convertUniqueIdToString(ByteUtils.convertLongToUniqueId(uniqueId))); 520 connectionIsReady = false; 521 //deviceUniqueId = uniqueId; 522 connectionLost(); 523 continueLock.countDown(); 524 fireConnectionChanged("logoff"); 525 } 526 }; 527 528 String portName = portController.getRealPortName(); 529 log.info("Open BiDiB connection on \"{}\"", portName); 530 531 try { 532 if (!bidib.isOpened()) { 533 bidib.setResponseTimeout(1600); 534 bidib.open(portName, connectionListener, nodeListeners, messageListeners, transferListeners, context); 535 } 536 else { 537 // if we get here, we assume that the adapter has already opened the port just for scanning the device 538 // and that NO listeners have been registered. So just add them now. 539 // If one day we start to really use the listeners we would have to check if this is o.k. 540 portController.registerAllListeners(connectionListener, nodeListeners, messageListeners, transferListeners); 541 } 542 543 544 log.debug("the connection is now opened: {}", bidib.isOpened()); 545 546 if (isAsyncInit) { 547 // for async connections (netBiDiB) we have to wait here until the connection is paired 548 // the server has logged in and the nodes have been initialized. 549 550 // get the pairing timeout and then wait a bit longer to be sure... 551 final NetBidibLinkData clientLinkData = 552 context.get(Context.NET_BIDIB_CLIENT_LINK_DATA, NetBidibLinkData.class, null); 553 long timeout = clientLinkData.getRequestedPairingTimeout(); 554 555 boolean success = continueLock.await(timeout + 10, TimeUnit.SECONDS); 556 if (pairingDialog != null) { 557 pairingDialog.dispose(); //just to be sure the dialog disappears... 558 pairingDialog = null; 559 } 560 if (!success || !curPairingResult.equals(PairingResult.PAIRED)) { 561 // an unpaired connection will be closed. 562 if (bidib.isOpened()) { 563 if (!success) { 564 log.warn("pairing or login timed out! Root node cannot be initialized - closing connection"); 565 } 566 else { 567 log.warn("pairing or login failed! Root node cannot be initialized - closing connection"); 568 } 569 bidib.close(); 570 } 571 return null; 572 } 573 } 574 else { 575 // even in non-async mode (all but netBiDiB) the node init is done in another thread 576 // so we wait here for the node init has been completed. 577 boolean success = continueLock.await(30, TimeUnit.SECONDS); 578 if (!success) { 579 log.warn("node init timeout!"); 580 } 581 } 582 583 if (!bidib.isOpened() || !connectionIsReady) { 584 log.info("connection is not ready - not initialized."); 585 return null; 586 } 587 588 // The connection is ready now. 589 // The nodes have already been initialized here from the connectionListener events. 590 591 log.info("registering shutdown task"); 592 ShutDownTask shutDownTask = new AbstractShutDownTask("BiDiB Shutdown Task") { 593 @Override 594 public void run() { 595 log.info("Shutdown Task - Terminate {}", getUserName()); 596 terminate(); 597 log.info("Shutdown task finished {}", getUserName()); 598 } 599 }; 600 InstanceManager.getDefault(ShutDownManager.class).register(shutDownTask); 601 602 return context; 603 604 } 605 catch (PortNotFoundException ex) { 606 log.error("The provided port was not found: {}. Verify that the BiDiB device is connected.", ex.getMessage()); 607 } 608 catch (Exception ex) { 609 log.error("Execute command failed: ", ex); // NOSONAR 610 } 611 return null; 612 } 613 614// public Long getUniqueId() { 615// return deviceUniqueId; 616// } 617 618 /** 619 * Check if the connection is ready to communicate. 620 * For netBiDiB this is the case only if the pairing was successful and 621 * the server has logged in. 622 * For all other connections the connection is ready if it has been 623 * successfully opened. 624 * 625 * @return true if the connection is ready 626 */ 627 public boolean isConnectionReady() { 628 return connectionIsReady; 629 } 630 631 /** 632 * Check of the connection is netBiDiB. 633 * 634 * @return true if this connection is netBiDiB 635 */ 636 public boolean isNetBiDiB() { 637 return isNetBiDiB; 638 } 639 640 /** 641 * Set the connection to lost state. 642 * All nodes will be removed and the components will be invalidated 643 */ 644 public void connectionLost() { 645 connectionIsReady = false; 646 nodeInitializer.connectionLost(); 647 } 648 649 // Methods used by BiDiB only 650 651 /** 652 * Check if the connection is detached i.e. it is opened, paired 653 * but the logon has been rejected. 654 * 655 * @return true if detached 656 */ 657 public boolean isDetached() { 658 if (bidib != null) { 659 try { 660 RootNode rootNode = bidib.getRootNode(); 661 return rootNode.getMasterNode().isDetached(); 662 } 663 catch (Exception e) { 664 log.trace("cannot determine detached flag: {}", e.toString()); 665 } 666 } 667 return false; 668 } 669 670 /** 671 * Set or remove the detached state. 672 * If logoff is requested (detach), a logon reject is sent to the device 673 * and all nodes will be notified that they are no more reachable (lost node). 674 * The connection remains active, but other clients may request a logon from 675 * the connected device. 676 * 677 * If a logon is requested (attach), the connected device is asked for a new logon. 678 * When the logon is received, the nodes will be re-initialized by the traffic 679 * controller. 680 * 681 * @param logon - true for logon (attach), false for logoff (detach) 682 */ 683 public void setLogon(boolean logon) { 684 Long uid; 685 try { 686 // get the JMRI uid 687 final NetBidibLinkData providedClientLinkData = 688 portController.getContext().get(Context.NET_BIDIB_CLIENT_LINK_DATA, NetBidibLinkData.class, null); 689 uid = providedClientLinkData.getUniqueId() & 0xFFFFFFFFFFL; 690 } 691 catch (Exception e) { 692 log.error("cannot determine our own Unique ID: {}", e.toString()); 693 return; 694 } 695 if (logon) { 696 bidib.attach(uid); 697 // after successful logon the node(s) will be newly initialized by the traffic controller 698 } 699 else { 700 // since there won't be any event fired upon a detached connection, we have to do it ourself 701 connectionLost(); 702 bidib.detach(uid); 703 bidib.getRootNode().getMasterNode().setDetached(true); 704 } 705 } 706 707 /** 708 * Add/Remove an ActionListener to be called when the connection has changed. 709 * 710 * @param l - an Object implementing the ActionListener interface 711 */ 712 public void addConnectionChangedListener(ActionListener l) { 713 synchronized (connectionChangedListeners) { 714 connectionChangedListeners.add(l); 715 } 716 } 717 718 public void removeConnectionChangedListener(ActionListener l) { 719 synchronized (connectionChangedListeners) { 720 connectionChangedListeners.remove(l); 721 } 722 } 723 724 private void fireConnectionChanged(String cmd) { 725 726 final Collection<ActionListener> safeListeners; 727 synchronized (connectionChangedListeners) { 728 safeListeners = CollectionUtils.newArrayList(connectionChangedListeners); 729 } 730 for (ActionListener l : safeListeners) { 731 l.actionPerformed(new ActionEvent(this, ActionEvent.ACTION_PERFORMED, cmd)); 732 } 733 } 734 735 /** 736 * Start jbidibc built-in local ping as a watchdog of the connection. 737 * If there is no response from the device, the connection will be closed. 738 */ 739 public void startLocalPing() { 740 if (useLocalPing) { 741 log.debug("Start the netBiDiB local ping worker on the rootNode."); 742 try { 743 744 if (bidib != null) { 745 bidib.getRootNode().setLocalPingEnabled(true); 746 747 bidib.getRootNode().startLocalPingWorker(true, localPingResult -> { 748 if (localPingResult.equals(Boolean.FALSE)) { 749 log.warn("The local pong was not received! Close connection"); 750 delayedCloseTimer.start(); 751 } 752 }); 753 } 754 } 755 catch (Exception ex) { 756 log.warn("Start the local ping worker failed.", ex); 757 } 758 } 759 else { 760 log.debug("local ping is disabled."); 761 } 762 } 763 764 // End Methods used by BiDiB only 765 766 767 Node debugSavedNode; 768 public void TEST(boolean a) {///////////////////DEBUG 769 log.debug("TEST {}", a); 770 String nodename = "XXXX"; 771 Node node = a ? debugSavedNode : getNodeByUserName(nodename); 772 if (node != null) { 773 if (a) { 774 nodeInitializer.nodeNew(node); 775 //nodeInitializer.nodeLost(node); 776 } 777 else { 778 debugSavedNode = node; 779 nodeInitializer.nodeLost(node); 780 } 781 } 782 } 783 784 /** 785 * Get Bidib Interface 786 * 787 * @return Bidib Interface 788 */ 789 public BidibInterface getBidib() { 790 return bidib; 791 } 792 793// convenience methods for node handling 794 795 /** 796 * Get the list of nodes found 797 * 798 * @return list of nodes 799 */ 800 public Map<Long, Node> getNodeList() { 801 return nodes; 802 } 803 804 /** 805 * Get node by unique id from nodelist 806 * 807 * @param uniqueId search for this 808 * @return node 809 */ 810 public Node getNodeByUniqueID(long uniqueId) { 811 return nodes.get(uniqueId); 812 } 813 814 /** 815 * Get node by node address from nodelist 816 * 817 * @param addr input to search 818 * @return node 819 */ 820 public Node getNodeByAddr(byte[] addr) { 821 for(Map.Entry<Long, Node> entry : nodes.entrySet()) { 822 Node node = entry.getValue(); 823 if (NodeUtils.isAddressEqual(node.getAddr(), addr)) { 824 return node; 825 } 826 } 827 return null; 828 } 829 830 /** 831 * Get node by node username from nodelist 832 * 833 * @param userName input to search 834 * @return node 835 */ 836 public Node getNodeByUserName(String userName) { 837 //log.debug("getNodeByUserName: [{}]", userName); 838 for(Map.Entry<Long, Node> entry : nodes.entrySet()) { 839 Node node = entry.getValue(); 840 //log.debug(" node: {}, usename: {}", node, node.getStoredString(StringData.INDEX_USERNAME)); 841 if (node.getStoredString(StringData.INDEX_USERNAME).equals(userName)) { 842 return node; 843 } 844 } 845 return null; 846 } 847 848 /** 849 * Get root node from nodelist 850 * 851 * @return node 852 */ 853 public Node getRootNode() { 854 byte[] addr = {0}; 855 return getNodeByAddr(addr); 856 } 857 858 /** 859 * A node suitable as a global programmer must be 860 * 861 * - a command station, 862 * - must support service mode programming and 863 * - must be a booster. 864 * - for other nodes than the global command station the local DCC generator 865 * must be switched on (MSG_BOOST_STAT returns this as "control") 866 * 867 * @param node to check 868 * 869 * @return true if the node is suitable as a global progreammer 870 */ 871 public boolean isGlobalProgrammerNode(Node node) { 872 873 if (NodeUtils.hasCommandStationFunctions(node.getUniqueId())) { 874// if (node.equals(getRootNode())) { //DEBUG 875// log.trace("---is root node: {}", node); 876// continue;//TEST: pretend that the root does not support service mode. 877// } 878 if (NodeUtils.hasCommandStationProgrammingFunctions(node.getUniqueId()) 879 && NodeUtils.hasBoosterFunctions(node.getUniqueId())) { 880 log.trace("node supports command station, programming and booster functions: {}", node); 881 if (node == getFirstCommandStationNode() || hasLocalDccEnabled(node)) { 882 return true; 883 } 884 } 885 } 886 return false; 887 } 888 889 /** 890 * Get the most probably only global programmer node (aka service mode node, used for prgramming track - PT, not for POM) 891 * If there are more than one suitable node, get the last one (reverse nodes list search). 892 * In this case the command station (probably the root node and first entry in the list) should 893 * probably not be used as a global programmer and the other have been added just for that purpose. 894 * TODO: the user should select the global programmer node if there multiple nodes suitable 895 * as a global programmer. 896 * 897 * 898 * @return programmer node or null if none available 899 */ 900 public Node getFirstGlobalProgrammerNode() { 901 //log.debug("find global programmer node"); 902 for(Map.Entry<Long, Node> entry : nodes.descendingMap().entrySet()) { 903 Node node = entry.getValue(); 904 //log.trace("node entry: {}", node); 905 if (isGlobalProgrammerNode(node)) { 906 synchronized (progTimer) { 907 log.debug("global programmer found: {}", node); 908 progTimer.restart(); 909 return node; 910 } 911 } 912 } 913 return null; 914 } 915 916 /** 917 * Set the global programmer node to use. 918 * 919 * @param node to be used as global programmer node or null to remove the currentGlobalProgrammerNode 920 * 921 * @return true if node is a suitable global programmer node, currentGlobalProgrammerNode is set to that node. false if it is not. 922 */ 923 public boolean setCurrentGlobalProgrammerNode(Node node) { 924 if (node == null || isGlobalProgrammerNode(node)) { 925 currentGlobalProgrammerNode = node; 926 return true; 927 } 928 return false; 929 } 930 931 /** 932 * Get the cached global programmer node. If there is no, try to find a suitable node. 933 * Note that the global programmer node may dynamically change by user settings. 934 * Be sure to update or invalidate currentGlobalProgrammerNode. 935 * 936 * @return the current global programmer node or null if none available. 937 */ 938 public Node getCurrentGlobalProgrammerNode() { 939 //log.trace("get current global programmer node: {}", currentGlobalProgrammerNode); 940 if (currentGlobalProgrammerNode == null) { 941 currentGlobalProgrammerNode = getFirstGlobalProgrammerNode(); 942 } 943 return currentGlobalProgrammerNode; 944 } 945 946 /** 947 * Ask the node if the local DCC generator is enabled. The state is returned as a 948 * BoosterControl value of LOCAL. 949 * Note that this function is expensive since it gets the information directly 950 * from the node and receiving a MSG_BOOST_STATE message. 951 * 952 * As far as I know (2023) there is only one device that supports the dynamically DCC generator switching: the Fichtelbahn ReadyBoost 953 * 954 * @param node to ask 955 * @return true if local DCC generator is enabled, false if the booster is connected to the 956 * global command station. 957 * 958 */ 959 private boolean hasLocalDccEnabled(Node node) { 960 if ((bidib instanceof SimulationBidib)) { // **** this a hack for the simulator since it will never return LOCAL dcc... 961 return true; 962 } 963 boolean hasLocalDCC = false; 964 // check if the node has a local DCC generator 965 BoosterNode bnode = getBidib().getBoosterNode(node); 966 if (bnode != null) { 967 try { 968 BoosterStateData bdata = bnode.queryState(); //send and wait for response 969 log.trace("Booster state data: {}", bdata); 970 if (bdata.getControl() == BoosterControl.LOCAL) { 971 hasLocalDCC = true; //local DCC generator is enabled 972 } 973 } 974 catch (ProtocolException e) {} 975 } 976 log.debug("node has local DCC enabled: {}, {}", hasLocalDCC, node); 977 return hasLocalDCC; 978 } 979 980 981 /** 982 * Get the first and most probably only command station node (also used for Programming on the Main - POM) 983 * A cached value is returned here for performance reasons since the function is called very often. 984 * We don't expect the command station to change. 985 * 986 * @return command station node 987 */ 988 public Node getFirstCommandStationNode() { 989 if (cachedCommandStationNode == null) { 990 for(Map.Entry<Long, Node> entry : nodes.entrySet()) { 991 Node node = entry.getValue(); 992 if (NodeUtils.hasCommandStationFunctions(node.getUniqueId())) { 993 log.trace("node has command station functions: {}", node); 994 cachedCommandStationNode = node; 995 break; 996 } 997 } 998 } 999 return cachedCommandStationNode; 1000 } 1001 1002 /** 1003 * Get the first booster node. 1004 * There may be more booster nodes, so prefer the command station and then try the others 1005 * @return booster node 1006 */ 1007 public Node getFirstBoosterNode() { 1008 Node node = getFirstCommandStationNode(); 1009 log.trace("getFirstBoosterNode: CS is {}", node); 1010 if (node != null && NodeUtils.hasBoosterFunctions(node.getUniqueId())) { 1011 // if the command station has a booster, use this one 1012 log.trace("CS node also has booster functions: {}", node); 1013 return node; 1014 } 1015 // if the command station does not have a booster, try to find another node with booster capability 1016 for(Map.Entry<Long, Node> entry : nodes.entrySet()) { 1017 node = entry.getValue(); 1018 if (NodeUtils.hasBoosterFunctions(node.getUniqueId())) { 1019 log.trace("node has booster functions: {}", node); 1020 return node; 1021 } 1022 } 1023 return null; 1024 } 1025 1026 /** 1027 * Get the first output node - a node that can control LC ports. 1028 * TODO: the method does not make much sense and its only purpose is to check if we have an output node at all. 1029 * Therefor it should be converted to "hasOutputNode" or similar. 1030 * @return output node 1031 */ 1032 public Node getFirstOutputNode() { 1033 for(Map.Entry<Long, Node> entry : nodes.entrySet()) { 1034 Node node = entry.getValue(); 1035 if (NodeUtils.hasAccessoryFunctions(node.getUniqueId()) || NodeUtils.hasSwitchFunctions(node.getUniqueId())) { 1036 log.trace("node has output functions (accessories or ports): {}", node); 1037 return node; 1038 } 1039 } 1040 return null; 1041 } 1042 1043 /** 1044 * Check if we have at least one node capable of Accessory functions 1045 * @return true or false 1046 */ 1047 public boolean hasAccessoryNode() { 1048 for(Map.Entry<Long, Node> entry : nodes.entrySet()) { 1049 Node node = entry.getValue(); 1050 if (NodeUtils.hasAccessoryFunctions(node.getUniqueId())) { 1051 log.trace("node has accessory functions: {}", node); 1052 return true; 1053 } 1054 } 1055 return false; 1056 } 1057 1058// convenience methods for feature handling 1059 1060 /** 1061 * Find a feature for given node. 1062 * 1063 * @param node selected node 1064 * @param requestedFeatureId as integer 1065 * @return a Feature object or null if the node does not have the feature at all 1066 */ 1067 public Feature findNodeFeature(Node node, final int requestedFeatureId) { 1068 return Feature.findFeature(node.getFeatures(), requestedFeatureId); 1069 } 1070 1071 /** 1072 * Get the feature value of a node. 1073 * 1074 * @param node selected node 1075 * @param requestedFeatureId feature to get 1076 * @return the feature value as integer or 0 if the node does not have the feature 1077 */ 1078 public int getNodeFeature(Node node, final int requestedFeatureId) { 1079 Feature f = Feature.findFeature(node.getFeatures(), requestedFeatureId); 1080 if (f == null) { 1081 return 0; 1082 } 1083 else { 1084 return f.getValue(); 1085 } 1086 } 1087 1088 // this is here only as a workaround until PortModelEnum.getPortModel eventually will support flat_extended itself 1089 public PortModelEnum getPortModel(final Node node) { 1090 if (PortModelEnum.getPortModel(node) == PortModelEnum.type) { 1091 return PortModelEnum.type; 1092 } 1093 else { 1094 if (node.getPortFlatModel() >= 256) { 1095 return PortModelEnum.flat_extended; 1096 } 1097 else { 1098 return PortModelEnum.flat; 1099 } 1100 } 1101 } 1102 1103 // convenience methods to handle Message Listeners 1104 1105 /** 1106 * Add a message Listener to the connection 1107 * @param messageListener to be added 1108 */ 1109 public void addMessageListener(MessageListener messageListener) { 1110 if (bidib != null) { 1111 log.trace("addMessageListener called!"); 1112 BidibMessageProcessor rcv = bidib.getBidibMessageProcessor(); 1113 if (rcv != null) { 1114 rcv.addMessageListener(messageListener); 1115 } 1116 } 1117 } 1118 1119 /** 1120 * Remove a message Listener from the connection 1121 * @param messageListener to be removed 1122 */ 1123 public void removeMessageListener(MessageListener messageListener) { 1124 if (bidib != null) { 1125 log.trace("removeMessageListener called!"); 1126 BidibMessageProcessor rcv = bidib.getBidibMessageProcessor(); 1127 if (rcv != null) { 1128 rcv.removeMessageListener(messageListener); 1129 } 1130 } 1131 } 1132 1133 /** 1134 * Add a raw message Listener to the connection 1135 * @param rawMessageListener to be added 1136 */ 1137 public void addRawMessageListener(RawMessageListener rawMessageListener) { 1138 if (bidib != null) { 1139 bidib.addRawMessageListener(rawMessageListener); 1140 } 1141 } 1142 1143 /** 1144 * Remove a raw message Listener from the connection 1145 * @param rawMessageListener to be removed 1146 */ 1147 public void removeRawMessageListener(RawMessageListener rawMessageListener) { 1148 if (bidib != null) { 1149 bidib.removeRawMessageListener(rawMessageListener); 1150 } 1151 } 1152 1153 1154// Config and ConfigX handling 1155// NOTE: All of these methods should be either obsolete to moved to BiDiBOutputMessageHandler 1156 1157 private int getTypeCount(Node node, LcOutputType type) { 1158 int id; 1159 switch (type) { 1160 case SWITCHPORT: 1161 case SWITCHPAIRPORT: 1162 id = BidibLibrary.FEATURE_CTRL_SWITCH_COUNT; 1163 break; 1164 case LIGHTPORT: 1165 id = BidibLibrary.FEATURE_CTRL_LIGHT_COUNT; 1166 break; 1167 case SERVOPORT: 1168 id = BidibLibrary.FEATURE_CTRL_SERVO_COUNT; 1169 break; 1170 case SOUNDPORT: 1171 id = BidibLibrary.FEATURE_CTRL_SOUND_COUNT; 1172 break; 1173 case MOTORPORT: 1174 id = BidibLibrary.FEATURE_CTRL_MOTOR_COUNT; 1175 break; 1176 case ANALOGPORT: 1177 id = BidibLibrary.FEATURE_CTRL_ANALOGOUT_COUNT; 1178 break; 1179 case BACKLIGHTPORT: 1180 id = BidibLibrary.FEATURE_CTRL_BACKLIGHT_COUNT; 1181 break; 1182 case INPUTPORT: 1183 id = BidibLibrary.FEATURE_CTRL_INPUT_COUNT; 1184 break; 1185 default: 1186 return 0; 1187 } 1188 return getNodeFeature(node, id); 1189 } 1190 1191// semi-synchroneous methods using MSG_LC_CONFIGX_GET_ALL / MSG_LC_CONFIGX_GET (or MSG_LC_CONFIG_GET for older nodes) - use for init only 1192// semi-synchroneous means, that this method waits until all data has been received, 1193// but the data itself is delivered through the MessageListener to each registered components (e.g. BiDiBLight) 1194// Therefor this method must only be called after all component managers have initialized all components and thus their message listeners 1195 1196 /** 1197 * Request CONFIGX from all ports on all nodes of this connection. 1198 * Returns after all data has been received. 1199 * Received data is delivered to registered Message Listeners. 1200 */ 1201 public void allPortConfigX() { 1202 log.debug("{}: get alle LC ConfigX", getUserName()); 1203 for(Map.Entry<Long, Node> entry : nodes.entrySet()) { 1204 Node node = entry.getValue(); 1205 if (NodeUtils.hasSwitchFunctions(node.getUniqueId())) { 1206 getAllPortConfigX(node, null); 1207 } 1208 } 1209 } 1210 1211 /** 1212 * Request CONFIGX from all ports on a given node and possibly only for a given type. 1213 * 1214 * @param node requested node 1215 * @param type - if null, request all port types 1216 * @return Note: always returns null, since data is not collected synchroneously but delivered to registered Message Listeners. 1217 */ 1218 public List<LcConfigX> getAllPortConfigX(Node node, LcOutputType type) { 1219 // get all ports of a type or really all ports if type = null or flat addressing is enabled 1220 List<LcConfigX> portConfigXList = null; 1221 int numPorts; 1222 try { 1223 if (node.getProtocolVersion().isHigherThan(ProtocolVersion.VERSION_0_5)) { //ConfigX is available since V0.6 1224 if (node.isPortFlatModelAvailable()) { //flat addressing 1225 numPorts = node.getPortFlatModel(); 1226 if (numPorts > 0) { 1227 bidib.getNode(node).getAllConfigX(getPortModel(node), type, 0, numPorts); 1228 } 1229 } 1230 else { //type based addressing 1231 for (LcOutputType t : LcOutputType.values()) { 1232 if ( (type == null || type == t) && t.hasPortStatus() && t.getType() <= 15) { 1233 numPorts = getTypeCount(node, t); 1234 if (numPorts > 0) { 1235 bidib.getNode(node).getAllConfigX(getPortModel(node), t, 0, numPorts); 1236 } 1237 } 1238 } 1239 } 1240 } 1241 else { //old Config - type based adressing only 1242 for (LcOutputType t : LcOutputType.values()) { 1243 if ( (type == null || type == t) && t.hasPortStatus() && t.getType() <= 15) { 1244 numPorts = getTypeCount(node, t); 1245 if (numPorts > 0) { 1246 int[] plist = new int[numPorts]; 1247 for (int i = 0; i < numPorts; i++) { 1248 plist[i] = i; 1249 } 1250 bidib.getNode(node).getConfigBulk(PortModelEnum.type, t, plist); 1251 } 1252 } 1253 } 1254 } 1255 } catch (ProtocolException e) { 1256 log.error("getAllConfigX message failed:", e); 1257 } 1258 return portConfigXList; 1259 } 1260 1261 /** 1262 * Request CONFIGX if a given port on a given node and possibly only for a given type. 1263 * 1264 * @param node requested node 1265 * @param portAddr as an integer 1266 * @param type - if null, request all port types 1267 * @return Note: always returns null, since data is not collected synchroneously but delivered to registered Message Listeners. 1268 */ 1269 public LcConfigX getPortConfigX(Node node, int portAddr, LcOutputType type) { 1270 // synchroneous method using MSG_LC_CONFIGX_GET - use for init only 1271 try { 1272 if (node.getProtocolVersion().isHigherThan(ProtocolVersion.VERSION_0_5)) { //ConfigX is available since V0.6 1273 if (node.isPortFlatModelAvailable()) { 1274 bidib.getNode(node).getConfigXBulk(getPortModel(node), type, 2 /*Window Size*/, portAddr); 1275 } 1276 else { 1277 for (LcOutputType t : LcOutputType.values()) { 1278 if ( (type == null || type == t) && t.hasPortStatus() && t.getType() <= 15) { 1279 bidib.getNode(node).getConfigXBulk(getPortModel(node), t, 2 /*Window Size*/, portAddr); 1280 } 1281 } 1282 } 1283 } 1284 else { 1285 for (LcOutputType t : LcOutputType.values()) { 1286 if ( (type == null || type == t) && t.hasPortStatus() && t.getType() <= 15) { 1287 bidib.getNode(node).getConfigBulk(PortModelEnum.type, t, portAddr); 1288 } 1289 } 1290 } 1291 } catch (ProtocolException e) { 1292 log.error("getConfigXBulk message failed", e); 1293 } 1294 return null; ////////TODO remove return value completely 1295 } 1296 1297 /** 1298 * Convert a CONFIG object to a CONFIGX object. 1299 * This is a convenience method so the JMRI components need only to handle the CONFIGX format 1300 * 1301 * @param node context node 1302 * @param lcConfig the LcConfig object 1303 * @return a new LcConfigX object 1304 */ 1305 public LcConfigX convertConfig2ConfigX(Node node, LcConfig lcConfig) { 1306 Map<Byte, PortConfigValue<?>> portConfigValues = new HashMap<>(); 1307 BidibPort bidibPort; 1308 PortModelEnum model; 1309 if (node.isPortFlatModelAvailable()) { 1310 model = PortModelEnum.flat; 1311 byte portType = lcConfig.getOutputType(model).getType(); 1312 int portMap = 1 << portType; //set the corresponding bit only 1313 ReconfigPortConfigValue pcfg = new ReconfigPortConfigValue(portType, portMap); 1314 portConfigValues.put(BidibLibrary.BIDIB_PCFG_RECONFIG, pcfg); 1315 } 1316 else { 1317 model = PortModelEnum.type; 1318 } 1319 bidibPort = BidibPort.prepareBidibPort(model, lcConfig.getOutputType(model), lcConfig.getOutputNumber(model)); 1320 // fill portConfigValues from lcConfig 1321 switch (lcConfig.getOutputType(model)) { 1322 case SWITCHPORT: 1323 if (getNodeFeature(node, BidibLibrary.FEATURE_SWITCH_CONFIG_AVAILABLE) > 0) { 1324 byte ioCtrl = ByteUtils.getLowByte(lcConfig.getValue1()); //BIDIB_PCFG_IO_CTRL - this is not supported for ConfigX any more 1325 byte ticks = ByteUtils.getLowByte(lcConfig.getValue2()); //BIDIB_PCFG_TICKS 1326 byte switchControl = (2 << 4) | 2; //tristate 1327 switch (ioCtrl) { 1328 case 0: //simple output 1329 ticks = 0; //disable 1330 switchControl = (1 << 4); //1 << 4 | 0 1331 break; 1332 case 1: //high pulse (same than simple output, but turns off after ticks 1333 switchControl = (1 << 4); //1 << 4 | 0 1334 break; 1335 case 2: //low pulse 1336 switchControl = 1; // 0 << 4 | 1 1337 break; 1338 case 3: //tristate 1339 ticks = 0; 1340 break; 1341 default: 1342 // same as tristate TODO: Support 4 (pullup) and 5 (pulldown) - port is an input then (??, spec not clear) 1343 ticks = 0; 1344 break; 1345 } 1346 BytePortConfigValue pcfgTicks = new BytePortConfigValue(ticks); 1347 BytePortConfigValue pcfgSwitchControl = new BytePortConfigValue(switchControl); 1348 portConfigValues.put(BidibLibrary.BIDIB_PCFG_TICKS, pcfgTicks); 1349 portConfigValues.put(BidibLibrary.BIDIB_PCFG_SWITCH_CTRL, pcfgSwitchControl); 1350 } 1351 break; 1352 case LIGHTPORT: 1353 BytePortConfigValue pcfgLevelPortOff = new BytePortConfigValue(ByteUtils.getLowByte(lcConfig.getValue1())); 1354 BytePortConfigValue pcfgLevelPortOn = new BytePortConfigValue(ByteUtils.getLowByte(lcConfig.getValue2())); 1355 BytePortConfigValue pcfgDimmDown = new BytePortConfigValue(ByteUtils.getLowByte(lcConfig.getValue3())); 1356 BytePortConfigValue pcfgDimmUp = new BytePortConfigValue(ByteUtils.getLowByte(lcConfig.getValue4())); 1357 portConfigValues.put(BidibLibrary.BIDIB_PCFG_LEVEL_PORT_OFF, pcfgLevelPortOff); 1358 portConfigValues.put(BidibLibrary.BIDIB_PCFG_LEVEL_PORT_ON, pcfgLevelPortOn); 1359 portConfigValues.put(BidibLibrary.BIDIB_PCFG_DIMM_DOWN, pcfgDimmDown); 1360 portConfigValues.put(BidibLibrary.BIDIB_PCFG_DIMM_UP, pcfgDimmUp); 1361 break; 1362 case SERVOPORT: 1363 BytePortConfigValue pcfgServoAdjL = new BytePortConfigValue(ByteUtils.getLowByte(lcConfig.getValue1())); 1364 BytePortConfigValue pcfgServoAdjH = new BytePortConfigValue(ByteUtils.getLowByte(lcConfig.getValue2())); 1365 BytePortConfigValue pcfgServoSpeed = new BytePortConfigValue(ByteUtils.getLowByte(lcConfig.getValue3())); 1366 portConfigValues.put(BidibLibrary.BIDIB_PCFG_SERVO_ADJ_L, pcfgServoAdjL); 1367 portConfigValues.put(BidibLibrary.BIDIB_PCFG_SERVO_ADJ_H, pcfgServoAdjH); 1368 portConfigValues.put(BidibLibrary.BIDIB_PCFG_SERVO_SPEED, pcfgServoSpeed); 1369 break; 1370 case BACKLIGHTPORT: 1371 BytePortConfigValue pcfgDimmDown2 = new BytePortConfigValue(ByteUtils.getLowByte(lcConfig.getValue1())); 1372 BytePortConfigValue pcfgDimmUp2 = new BytePortConfigValue(ByteUtils.getLowByte(lcConfig.getValue2())); 1373 BytePortConfigValue pcfgOutputMap = new BytePortConfigValue(ByteUtils.getLowByte(lcConfig.getValue3())); 1374 portConfigValues.put(BidibLibrary.BIDIB_PCFG_DIMM_DOWN, pcfgDimmDown2); 1375 portConfigValues.put(BidibLibrary.BIDIB_PCFG_DIMM_UP, pcfgDimmUp2); 1376 portConfigValues.put(BidibLibrary.BIDIB_PCFG_OUTPUT_MAP, pcfgOutputMap); 1377 break; 1378 case INPUTPORT: 1379 // not really specified, but seems logical... 1380 byte ioCtrl = ByteUtils.getLowByte(lcConfig.getValue1()); //BIDIB_PCFG_IO_CTRL - this is not supported for ConfigX any more 1381 byte inputControl = 1; //active HIGH 1382 switch (ioCtrl) { 1383 case 4: //pullup 1384 inputControl = 2; //active LOW + Pullup 1385 break; 1386 case 5: //pulldown 1387 inputControl = 3; //active HIGH + Pulldown 1388 break; 1389 default: 1390 // do nothing, leave inputControl at 1 1391 break; 1392 } 1393 BytePortConfigValue pcfgInputControl = new BytePortConfigValue(inputControl); 1394 portConfigValues.put(BidibLibrary.BIDIB_PCFG_INPUT_CTRL, pcfgInputControl); 1395 break; 1396 default: 1397 break; 1398 } 1399 LcConfigX configX = new LcConfigX(bidibPort, portConfigValues); 1400 return configX; 1401 } 1402 1403 // Asynchronous methods to request the status of all BiDiB ports 1404 // For this reason there is no need to query FEATURE_CTRL_PORT_QUERY_AVAILABLE, since without this feature there would simply be no answer for the port. 1405 1406 /** 1407 * Request LC_STAT from all ports on all nodes of this connection. 1408 * Returns immediately. 1409 * Received data is delivered to registered Message Listeners. 1410 */ 1411 public void allPortLcStat() { 1412 log.debug("{}: get alle LC stat", getUserName()); 1413 for(Map.Entry<Long, Node> entry : nodes.entrySet()) { 1414 Node node = entry.getValue(); 1415 if (NodeUtils.hasSwitchFunctions(node.getUniqueId())) { 1416 portLcStat(node, 0xFFFF); 1417 } 1418 } 1419 } 1420 1421 /** 1422 * Request LC_STAT from all ports on a given node. 1423 * Returns immediately. 1424 * Received data is delivered to registered Message Listeners. 1425 * The differences for the addressing model an the old LC_STAT handling are hidden to the caller. 1426 * 1427 * @param node selected node 1428 * @param typemask a 16 bit type mask where each bit represents a type, Bit0 is SWITCHPORT, Bit1 is LIGHTPORT and so on. Bit 15 is INPUTPORT. 1429 * Return LC_STAT only for ports which are selected on the type mask (the correspondend bit is set). 1430 */ 1431 public void portLcStat(Node node, int typemask) { 1432 if (NodeUtils.hasSwitchFunctions(node.getUniqueId())) { 1433 BidibRequestFactory rf = getBidib().getRootNode().getRequestFactory(); 1434 if (node.getProtocolVersion().isHigherThan(ProtocolVersion.VERSION_0_6)) { 1435 // fast bulk query of all ports (new in bidib protocol version 0.7) 1436// int numPorts; 1437// if (node.isPortFlatModelAvailable()) { 1438// numPorts = node.getPortFlatModel(); 1439// if (numPorts > 0) { 1440// //BidibCommandMessage m = (BidibCommandMessage)rf.createPortQueryAll(typemask, 0, numPorts); 1441// BidibCommandMessage m = (BidibCommandMessage)rf.createPortQueryAll(typemask, 0, 0xFFFF); 1442// sendBiDiBMessage(m, node); 1443// } 1444// } 1445// else { //type based addressing 1446// for (LcOutputType t : LcOutputType.values()) { 1447// int tmask = 1 << t.getType(); 1448// if ( ((tmask & typemask) != 0) && t.hasPortStatus() && t.getType() <= 15) { 1449// numPorts = getTypeCount(node, t); 1450// if (numPorts > 0) { 1451// // its not clear how PORT_QUERY_ALL is defined for type based addressing 1452// // so we try the strictest way 1453// BidibPort fromPort = BidibPort.prepareBidibPort(PortModelEnum.type, t, 0); 1454// BidibPort toPort = BidibPort.prepareBidibPort(PortModelEnum.type, t, numPorts); 1455// int from = ByteUtils.getWORD(fromPort.getValues()); 1456// int to = ByteUtils.getWORD(toPort.getValues()); 1457// BidibCommandMessage m = (BidibCommandMessage)rf.createPortQueryAll(tmask, from, to); 1458// //BidibCommandMessage m = (BidibCommandMessage)rf.createPortQueryAll(typemask, 0, 0xFFFF); 1459// sendBiDiBMessage(m, node); 1460// //break; 1461// } 1462// } 1463// } 1464// } 1465 // just query everything 1466 BidibCommandMessage m = rf.createPortQueryAll(typemask, 0, 0xFFFF); 1467 sendBiDiBMessage(m, node); 1468 } 1469 else { 1470 // old protocol versions (<= 0.6 - request every single port 1471 int numPorts; 1472 if (node.isPortFlatModelAvailable()) { 1473 // since flat addressing is only available since version 0.6, this is only possible with exactly version 0.6 1474 numPorts = node.getPortFlatModel(); 1475 for (int addr = 0; addr < numPorts; addr++) { 1476 BidibCommandMessage m = rf.createLcPortQuery(getPortModel(node), null, addr); 1477 sendBiDiBMessage(m, node); 1478 } 1479 } 1480 else { //type based adressing 1481 for (LcOutputType t : LcOutputType.values()) { 1482 int tmask = 1 << t.getType(); 1483 if ( ((tmask & typemask) != 0) && t.hasPortStatus() && t.getType() <= 7) { //outputs only - for old protocol version 1484 numPorts = getTypeCount(node, t); 1485 for (int addr = 0; addr < numPorts; addr++) { 1486 BidibCommandMessage m = rf.createLcPortQuery(getPortModel(node), t, addr); 1487 sendBiDiBMessage(m, node); 1488 } 1489 } 1490 } 1491 // inputs have a separate message type in old versions: MSG_LC_KEY_QUERY 1492 LcOutputType t = LcOutputType.INPUTPORT; 1493 int tmask = 1 << t.getType(); 1494 if ( ((tmask & typemask) != 0) ) { 1495 numPorts = getTypeCount(node, t); 1496 for (int addr = 0; addr < numPorts; addr++) { 1497 BidibCommandMessage m = rf.createLcKey(addr); 1498 sendBiDiBMessage(m, node); 1499 } 1500 } 1501 } 1502 } 1503 } 1504 } 1505 1506// Asynchronous methods to request the status of all BiDiB feedback channels 1507 1508 public void allAccessoryState() { 1509 log.debug("{}: get alle accessories", getUserName()); 1510 for(Map.Entry<Long, Node> entry : nodes.entrySet()) { 1511 Node node = entry.getValue(); 1512 if (NodeUtils.hasAccessoryFunctions(node.getUniqueId())) { 1513 accessoryState(node); 1514 } 1515 } 1516 } 1517 1518 public void accessoryState(Node node) { 1519 int accSize = getNodeFeature(node, BidibLibrary.FEATURE_ACCESSORY_COUNT); 1520 if (NodeUtils.hasAccessoryFunctions(node.getUniqueId()) && accSize > 0 ) { 1521 log.info("Requesting accessory status on node {}", node); 1522 for (int addr = 0; addr < accSize; addr++) { 1523 sendBiDiBMessage(new AccessoryGetMessage(addr), node); 1524 } 1525 } 1526 } 1527 1528 /** 1529 * Request Feedback Status (called BM status in BiDiB - BM (Belegtmelder) is german for "feedback") from all ports on all nodes of this connection. 1530 * Returns immediately. 1531 * Received data is delivered to registered Message Listeners. 1532 */ 1533 public void allFeedback() { 1534 //log.debug("{}: get alle feedback", getUserName()); 1535 for(Map.Entry<Long, Node> entry : nodes.entrySet()) { 1536 Node node = entry.getValue(); 1537 if (NodeUtils.hasFeedbackFunctions(node.getUniqueId())) { 1538 feedback(node); 1539 } 1540 } 1541 } 1542 1543 /** 1544 * Request Feedback Status (called BM status in BiDiB - BM (Belegtmelder) is german for "feedback") from all ports on a given node. 1545 * Returns immediately. 1546 * Received data is delivered to registered Message Listeners. 1547 * 1548 * @param node selected node 1549 */ 1550 public void feedback(Node node) { 1551 int bmSize = getNodeFeature(node, BidibLibrary.FEATURE_BM_SIZE); 1552 if (NodeUtils.hasFeedbackFunctions(node.getUniqueId()) && bmSize > 0 ) { 1553 log.info("Requesting feedback status on node {}", node); 1554 sendBiDiBMessage(new FeedbackGetRangeMessage(0, bmSize), node); 1555 } 1556 } 1557 1558// End of obsolete Methods 1559 1560// BiDiB Message handling 1561 1562 /** 1563 * Forward a preformatted BiDiBMessage to the actual interface. 1564 * 1565 * @param m Message to send; 1566 * @param node BiDiB node to send the message to 1567 */ 1568 public void sendBiDiBMessage(BidibCommandMessage m, Node node) { 1569 if (node == null) { 1570 log.error("node is undefined! - can't send message."); 1571 return; 1572 } 1573 log.trace("sendBiDiBMessage: {} on node {}", m, node); 1574 // be sure that the node is in correct mode 1575 if (checkProgMode((m.getType() == BidibLibrary.MSG_CS_PROG), node) >= 0) { 1576 try { 1577 log.trace(" bidib node: {}", getBidib().getNode(node)); 1578 BidibNodeAccessor.sendNoWait(getBidib().getNode(node), m); 1579 } catch (ProtocolException e) { 1580 log.error("sending BiDiB message failed", e); 1581 } 1582 } 1583 else { 1584 log.error("switching to or from PROG mode (global programmer) failed!"); 1585 } 1586 } 1587 1588 /** 1589 * Check if the command station is in the requested state (Normal, PT) 1590 * If the command station is not in the requested state, a message is sent to BiDiB to switch to the requested state. 1591 * 1592 * @param needProgMode true if we request the command station to be in programming state, false if normal state is requested 1593 * @param node selected node 1594 * @return 0 if nothing to do, 1 if state has been changed, -1 on error 1595 */ 1596 public synchronized int checkProgMode(boolean needProgMode, Node node) { 1597 log.trace("checkProgMode: needProgMode: {}, node: {}", needProgMode, node); 1598 int hasChanged = 0; 1599 CommandStationState neededMode = needProgMode ? CommandStationState.PROG : mSavedMode; 1600 if (needProgMode != mIsProgMode.get()) { 1601 Node progNode = getCurrentGlobalProgrammerNode(); 1602 if (node == progNode) { 1603 Node csNode = getFirstCommandStationNode(); 1604 1605 log.debug("use global programmer node: {}", progNode); 1606 CommandStationNode progCsNode = getBidib().getCommandStationNode(progNode); //check if the programmer node also a command station - should have been tested before anyway 1607 if (progCsNode == null) { //just in case... 1608 currentGlobalProgrammerNode = null; 1609 hasChanged = -1; 1610 } 1611 else { 1612 log.debug("change command station mode to PROG? {}", needProgMode); 1613 if (needProgMode) { 1614 if (node == csNode) { 1615 // if we have to switch to prog mode, disable watchdog timer - but only, if we switch the command station 1616 setWatchdogTimer(false); 1617 } 1618 } 1619 try { 1620 CommandStationState CurrentMode = progCsNode.setState(neededMode); //send and wait for response 1621 synchronized (mIsProgMode) { 1622 if (!needProgMode) { 1623 mSavedMode = CurrentMode; 1624 } 1625 mIsProgMode.set(needProgMode); 1626 } 1627 hasChanged = 1; 1628 } 1629 catch (ProtocolException e) { 1630 log.error("sending MSG_CS_STATE message failed", e); 1631 currentGlobalProgrammerNode = null; 1632 hasChanged = -1; 1633 } 1634 log.trace("new saved mode: {}, is ProgMode: {}", mSavedMode, mIsProgMode); 1635 } 1636 } 1637 } 1638 if (!mIsProgMode.get()) { 1639 synchronized (progTimer) { 1640 if (progTimer.isRunning()) { 1641 progTimer.stop(); 1642 log.trace("progTimer stopped."); 1643 } 1644 } 1645 setCurrentGlobalProgrammerNode(null); //invalidate programmer node so it must be evaluated again the next time 1646 } 1647 1648 return hasChanged; 1649 } 1650 1651 private void progTimeout() { 1652 log.trace("timeout - stop global programmer PROG mode - reset to {}", mSavedMode); 1653 checkProgMode(false, getCurrentGlobalProgrammerNode()); 1654 } 1655 1656 1657// BiDiB Watchdog 1658 1659 private class WatchdogTimerTask extends java.util.TimerTask { 1660 @Override 1661 public void run () { 1662 // If the timer times out, send MSG_CS_STATE_ON message 1663 synchronized (watchdogStatus) { 1664 if (watchdogStatus.get()) { //if still enabled 1665 sendBiDiBMessage(new CommandStationSetStateMessage(CommandStationState.GO), getFirstCommandStationNode()); 1666 } 1667 } 1668 } 1669 } 1670 1671 public final void setWatchdogTimer(boolean state) { 1672 synchronized (watchdogStatus) { 1673 Node csnode = getFirstCommandStationNode(); 1674 long timeout = 0; 1675 log.trace("setWatchdogTimer {} on node {}", state, csnode); 1676 if (csnode != null) { 1677 timeout = getNodeFeature(csnode, BidibLibrary.FEATURE_GEN_WATCHDOG) * 100L; //value in milliseconds 1678 log.trace("FEATURE_GEN_WATCHDOG in ms: {}", timeout); 1679 if (timeout < 2000) { 1680 timeout = timeout / 2; //half the devices watchdog timeout value for small values 1681 } 1682 else { 1683 timeout = timeout - 1000; //one second less the devices watchdog timeout value for larger values 1684 } 1685 } 1686 if (timeout > 0 && state) { 1687 log.debug("set watchdog TRUE, timeout: {} ms", timeout); 1688 watchdogStatus.set(true); 1689 if (watchdogTimer != null) { 1690 watchdogTimer.cancel(); 1691 } 1692 watchdogTimer = new WatchdogTimerTask(); // Timer used to periodically MSG_CS_STATE_ON 1693 jmri.util.TimerUtil.schedule(watchdogTimer, timeout, timeout); 1694 } 1695 else { 1696 log.debug("set watchdog FALSE, requested state: {}, timeout", state); 1697 watchdogStatus.set(false); 1698 if (watchdogTimer != null) { 1699 watchdogTimer.cancel(); 1700 } 1701 watchdogTimer = null; 1702 } 1703 } 1704 } 1705 1706// local ping (netBiDiB devices) 1707 1708 private void localPingTimeout() { 1709 log.trace("send local ping"); 1710 if (connectionIsReady) { 1711 sendBiDiBMessage(new LocalPingMessage(), getFirstCommandStationNode()); 1712 } 1713 else { 1714 localPingTimer.stop(); 1715 } 1716 } 1717 1718 1719 /** 1720 * Reference to the system connection memo. 1721 */ 1722 BiDiBSystemConnectionMemo mMemo = null; 1723 1724 /** 1725 * Get access to the system connection memo associated with this traffic 1726 * controller. 1727 * 1728 * @return associated systemConnectionMemo object 1729 */ 1730 public BiDiBSystemConnectionMemo getSystemConnectionMemo() { 1731 return (mMemo); 1732 } 1733 1734 /** 1735 * Set the system connection memo associated with this traffic controller. 1736 * 1737 * @param m associated systemConnectionMemo object 1738 */ 1739 public void setSystemConnectionMemo(BiDiBSystemConnectionMemo m) { 1740 mMemo = m; 1741 } 1742 1743// Command Station interface 1744 1745 /** 1746 * {@inheritDoc} 1747 */ 1748 @Override 1749 public String getSystemPrefix() { 1750 if (mMemo != null) { 1751 return mMemo.getSystemPrefix(); 1752 } 1753 return ""; 1754 } 1755 1756 /** 1757 * {@inheritDoc} 1758 */ 1759 @Override 1760 public String getUserName() { 1761 if (mMemo != null) { 1762 return mMemo.getUserName(); 1763 } 1764 return ""; 1765 } 1766 1767 /** 1768 * {@inheritDoc} 1769 * 1770 * Not supported! We probably don't need the command station interface at all... 1771 * ... besides perhaps consist control or DCC Signal Mast / Head ?? 1772 */ 1773 @Override 1774 public boolean sendPacket(byte[] packet, int repeats) { 1775 log.debug("sendPacket: {}, prefix: {}", packet, mMemo.getSystemPrefix()); 1776 if (packet != null && packet.length >= 4) { 1777 //log.debug("Addr: {}, aspect: {}, repeats: {}", NmraPacket.getAccSignalDecoderPktAddress(packet), packet[2], repeats); 1778 //log.debug("Addr: {}, aspect: {}, repeats: {}", NmraPacket.getAccDecoderPktAddress(packet), packet[2], repeats); 1779 log.debug("Addr: {}, addr type: {}, aspect: {}, repeats: {}", NmraPacket.extractAddressType(packet), NmraPacket.extractAddressNumber(packet), packet[2], repeats); 1780 //throw new UnsupportedOperationException("Not supported yet."); 1781 log.warn("sendPacket is not supported for BiDiB so far"); 1782 } 1783 return false; 1784 } 1785 1786// NOT USED for now 1787// /** 1788// * Get the Lower byte of a locomotive address from the decimal locomotive 1789// * address. 1790// */ 1791// public static int getDCCAddressLow(int address) { 1792// /* For addresses below 128, we just return the address, otherwise, 1793// we need to return the upper byte of the address after we add the 1794// offset 0xC000. The first address used for addresses over 127 is 0xC080*/ 1795// if (address < 128) { 1796// return (address); 1797// } else { 1798// int temp = address + 0xC000; 1799// temp = temp & 0x00FF; 1800// return temp; 1801// } 1802// } 1803// 1804// /** 1805// * Get the Upper byte of a locomotive address from the decimal locomotive 1806// * address. 1807// */ 1808// public static int getDCCAddressHigh(int address) { 1809// /* this isn't actually the high byte, For addresses below 128, we 1810// just return 0, otherwise, we need to return the upper byte of the 1811// address after we add the offset 0xC000 The first address used for 1812// addresses over 127 is 0xC080*/ 1813// if (address < 128) { 1814// return (0x00); 1815// } else { 1816// int temp = address + 0xC000; 1817// temp = temp & 0xFF00; 1818// temp = temp / 256; 1819// return temp; 1820// } 1821// } 1822 1823 1824// Shutdown function 1825 1826 protected void terminate () { 1827 log.debug("Cleanup starts {}", this); 1828 if (bidib == null || !bidib.isOpened()) { 1829 return; // no connection established 1830 } 1831 Node node = getCurrentGlobalProgrammerNode(); 1832 if (node != null) { 1833 checkProgMode(false, node); //possibly switch to normal mode 1834 } 1835 setWatchdogTimer(false); //stop watchdog 1836 // sending SYS_DISABLE disables all spontaneous messages and thus informs all nodes that the host will probably disappear 1837 try { 1838 log.info("sending sysDisable to {}", getRootNode()); 1839 bidib.getRootNode().sysDisable(); //Throws ProtocolException 1840 } 1841 catch (ProtocolException e) { 1842 log.error("unable to disable node", e); 1843 } 1844 1845 log.debug("Cleanup ends"); 1846 } 1847 1848// /** NO LONGER USED 1849// * Internal class to handle traffic controller cleanup. The primary task of 1850// * this thread is to make sure the DCC system has exited service mode when 1851// * the program exits. 1852// */ 1853// static class CleanupHook implements Runnable { 1854// 1855// BiDiBTrafficController tc; 1856// 1857// CleanupHook(BiDiBTrafficController tc) { 1858// this.tc = tc; 1859// } 1860// 1861// @Override 1862// public void run() { 1863// tc.terminate(); 1864// } 1865// } 1866// 1867 1868 private final static Logger log = LoggerFactory.getLogger(BiDiBTrafficController.class); 1869 1870}