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