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