001package jmri.jmrix.loconet; 002 003import java.util.EnumSet; 004import java.util.Hashtable; 005import java.util.concurrent.LinkedBlockingQueue; 006import jmri.DccLocoAddress; 007import jmri.DccThrottle; 008import jmri.LocoAddress; 009import jmri.SpeedStepMode; 010import jmri.ThrottleListener; 011import jmri.jmrix.AbstractThrottleManager; 012import org.slf4j.Logger; 013import org.slf4j.LoggerFactory; 014 015/** 016 * LocoNet implementation of a ThrottleManager. 017 * <p> 018 * Works in cooperation with the SlotManager, which actually handles the 019 * communications. 020 * 021 * @see SlotManager 022 * @author Bob Jacobsen Copyright (C) 2001 023 * @author B. Milhaupt, Copyright (C) 2018 024 */ 025public class LnThrottleManager extends AbstractThrottleManager implements SlotListener { 026 027 protected SlotManager slotManager; 028 protected LnTrafficController tc; 029 030 /** 031 * Constructor. Gets a reference to the LocoNet SlotManager. 032 * 033 * @param memo connection's memo 034 */ 035 public LnThrottleManager(LocoNetSystemConnectionMemo memo) { 036 super(memo); 037 this.slotManager = memo.getSlotManager(); 038 this.tc = memo.getLnTrafficController(); 039 requestList = new LinkedBlockingQueue<>(); 040 slotForAddress = new Hashtable<>(); 041 } 042 043 /** 044 * LocoNet allows multiple throttles for the same device. 045 * <p> 046 * {@inheritDoc} 047 * @return false always 048 */ 049 @Override 050 protected boolean singleUse() { 051 return false; 052 } 053 054 /** 055 * Display the Silent Stealing checkbox option in Throttles Preferences 056 */ 057 @Override 058 public boolean enablePrefSilentStealOption() { 059 return true; 060 } 061 062 /** 063 * Start creating a Throttle object. 064 * 065 * This returns directly, having arranged for the Throttle object to be 066 * delivered via callback since there are situations where the command 067 * station does not respond, (slots full, command station powered off, 068 * others?) this code will retry and then fail the request if no response 069 * occurs. 070 * 071 * @param address locomotive address to be controlled 072 * @param control true if throttle wishes to control the speed and direction 073 * of the loco. 074 */ 075 @Override 076 public void requestThrottleSetup(LocoAddress address, boolean control) { 077 log.debug("requestThrottleSetup: address {}, control {}", address, control); 078 // Checking requestOutstanding and setting it true must be atomic -- 079 // see its declaration below for the race this closes. 080 boolean handleNow; 081 synchronized (this) { 082 if (requestOutstanding) { 083 handleNow = false; 084 } else { 085 requestOutstanding = true; 086 handleNow = true; 087 } 088 } 089 if (handleNow) { 090 // handle this now 091 processThrottleSetupRequest(address, control); 092 } else { 093 try { 094 // queue this request for later. 095 requestList.put(new ThrottleRequest(address,control)); 096 } catch (InterruptedException ie) { 097 log.error("Interrupted while trying to store throttle request"); 098 synchronized (this) { 099 requestOutstanding = false; 100 } 101 } 102 } 103 } 104 105 /** 106 * Processes the next loco from the queue of requested locos for which to get 107 * a LocoNetThrottle. 108 */ 109 protected void processQueuedThrottleSetupRequest() { 110 // Same atomicity requirement as requestThrottleSetup() above. 111 boolean handleNow; 112 synchronized (this) { 113 if (!requestOutstanding && (requestList.size() != 0 )) { 114 requestOutstanding = true; 115 handleNow = true; 116 } else { 117 handleNow = false; 118 } 119 } 120 if (handleNow) { 121 try { 122 ThrottleRequest tr = requestList.take(); 123 processThrottleSetupRequest(tr.getAddress(), tr.getControl()); 124 } catch (InterruptedException ie) { 125 log.error("Interrupted while trying to process process throttle request"); 126 synchronized (this) { 127 requestOutstanding = false; 128 } 129 } 130 } 131 } 132 133 /** 134 * Begin the processing of a Throttle Request. 135 * 136 * @param address Loco address 137 * @param control whether the throttle object wants to control the loco 138 */ 139 private void processThrottleSetupRequest(LocoAddress address, boolean control) { 140 pendingRequestAddress = new DccLocoAddress(address.getNumber(), isLongAddress(address.getNumber())); 141 slotManager.slotFromLocoAddress(address.getNumber(), this); //first try 142 143 class RetrySetup implements Runnable { // setup for retries and failure check 144 145 final DccLocoAddress address; 146 final SlotListener list; 147 148 RetrySetup(DccLocoAddress address, SlotListener list) { 149 this.address = address; 150 this.list = list; 151 } 152 153 @Override 154 public void run() { 155 int attempts = 1; // already tried once above 156 // Was 10 (10s total) -- too short for a command station 157 // still working through a backlog of rapid sequential slot 158 // requests (e.g. building/tearing down a large consist). 159 // This is a dedicated background thread, so waiting longer 160 // here is safe. 161 int maxAttempts = 20; 162 while (attempts <= maxAttempts) { 163 try { 164 Thread.sleep(1000); // wait one second 165 } catch (InterruptedException ex) { 166 return; // stop waiting if slot is found or error occurs 167 } 168 String again = ""; 169 if (attempts < maxAttempts) { 170 slotManager.slotFromLocoAddress(address.getNumber(), list); 171 again = ", trying again."; // NOI18N 172 } 173 log.debug("No response to slot request for {}, attempt {} {}", address, attempts, again); 174 attempts++; 175 } 176 log.error("No response to slot request for {} after {} attempts.", address, attempts - 1); // NOI18N 177 // A listener throwing out of notifyFailedThrottleRequest() 178 // must not skip the cleanup below -- otherwise 179 // requestOutstanding stays stuck true forever and wedges 180 // every future throttle request for the rest of the session. 181 try { 182 failedThrottleRequest(address, "Failed to get response from command station"); 183 } catch (RuntimeException ex) { 184 log.error("Listener threw while handling failed throttle request for {} -- continuing anyway", address, ex); 185 } finally { 186 requestOutstanding = false; 187 pendingRequestAddress = null; 188 processQueuedThrottleSetupRequest(); 189 } 190 } 191 } 192 193 retrySetupThread = new Thread( 194 new RetrySetup(new DccLocoAddress(address.getNumber(), 195 isLongAddress(address.getNumber())), this)); 196 retrySetupThread.setName("LnThrottleManager RetrySetup " + address); 197 retrySetupThread.start(); 198 synchronized (this) { 199 waitingForNotification.put(address.getNumber(), retrySetupThread); 200 } 201 } 202 203 volatile Thread retrySetupThread; 204 205 // Address a pending slot request was actually made for. This manager 206 // is a single shared SlotListener for whatever address it's currently 207 // requesting, so a stale/delayed response from an earlier, already- 208 // completed request can otherwise get misattributed to the current 209 // one. Checked in notifyChangedSlot() before acting on a response; a 210 // mismatch is simply ignored since RetrySetup already re-issues the 211 // request every second regardless. 212 private volatile DccLocoAddress pendingRequestAddress = null; 213 214 Hashtable<Integer, Thread> waitingForNotification = new Hashtable<>(5); 215 216 Hashtable<Integer, LocoNetSlot> slotForAddress; 217 LinkedBlockingQueue<ThrottleRequest> requestList; 218 // volatile, and only ever check-and-set together with 219 // pendingRequestAddress inside synchronized(this) -- see 220 // requestThrottleSetup()/processQueuedThrottleSetupRequest(). Without 221 // that, two threads (e.g. the EDT building a consist and a separate 222 // JSON throttle request) can both observe this false at once and both 223 // proceed, each overwriting pendingRequestAddress and getting checked 224 // against the other's slot response. 225 volatile boolean requestOutstanding = false; 226 227 /** 228 * LocoNet does have a Dispatch function. 229 * 230 * @return true 231 */ 232 @Override 233 public boolean hasDispatchFunction() { 234 return true; 235 } 236 237 /** 238 * What speed modes are supported by this system? value should be xor of 239 * possible modes specified by the DccThrottle interface. 240 * 241 * @return an integer containing the combined speed step modes supported 242 */ 243 @Override 244 public EnumSet<SpeedStepMode> supportedSpeedModes() { 245 return EnumSet.of(SpeedStepMode.NMRA_DCC_128 246 , SpeedStepMode.NMRA_DCC_28 247 , SpeedStepMode.MOTOROLA_28 248 , SpeedStepMode.NMRA_DCC_14); 249 } 250 251 /** 252 * Get notification that an address has changed slot. This method creates a 253 * throttle for all ThrottleListeners of that address and notifies them via 254 * the ThrottleListener.notifyThrottleFound method. 255 * 256 * @param s LocoNet slot which has been changed 257 */ 258 @Override 259 public void notifyChangedSlot(LocoNetSlot s) { 260 log.debug("notifyChangedSlot - slot {}, slotStatus {}", s.getSlot(), Integer.toHexString(s.slotStatus())); 261 // This is invoked only if the SlotManager knows that the LnThrottleManager is 262 // interested in the address associated with this slot. 263 264 // Reject a response that doesn't match what's actually being 265 // waited for (see pendingRequestAddress above) -- RetrySetup's 266 // 1-second retry loop recovers naturally rather than this 267 // silently completing the wrong address's acquisition. 268 DccLocoAddress expected = pendingRequestAddress; 269 if (expected != null && s.locoAddr() != expected.getNumber()) { 270 log.warn("notifyChangedSlot(): requested slot for {} but got slot {} for address {} instead -- ignoring stale/mismatched response", 271 expected, s.getSlot(), s.locoAddr()); 272 return; 273 } 274 275 // need to check to see if the slot is in a suitable state for creating a throttle. 276 if (s.slotStatus() == LnConstants.LOCO_IN_USE) { 277 // loco is already in-use 278 log.warn("slot {} address {} is already in-use.", 279 s.getSlot(), s.locoAddr()); 280 // is the throttle ID the same as for this JMRI instance? If not, do not accept the slot. 281 if ((s.id() != 0) && s.id() != throttleID) { 282 // notify the LnThrottleManager about failure of acquisition. 283 // NEED TO TRIGGER THE NEW "STEAL REQUIRED" FUNCTIONALITY HERE 284 //note: throttle listener expects to have "callback" method notifyDecisionRequired 285 //invoked if a "steal" is required. Make that happen as part of the "acquisition" process 286 synchronized (this) { 287 slotForAddress.put(s.locoAddr(), s); 288 } 289 notifyStealRequest(s.locoAddr()); 290 return; 291 } 292 // shared throttle / already ours 293 notifyComplete(commitToAcquireThrottle(s),s); 294 return; 295 } 296 commitToAcquireThrottle(s); 297 } 298 299 /** 300 * Making progress in the process of acquiring a throttle. 301 * 302 * @param s slot to be acquired 303 */ 304 private DccThrottle commitToAcquireThrottle(LocoNetSlot s) { 305 // haven't identified a particular reason to refuse throttle acquisition at this time... 306 return createThrottle((LocoNetSystemConnectionMemo) adapterMemo, s); 307 // the rest is done when the write of the throttle ID has been acknowledged in the throttle 308 // by calling notifyComplete 309 } 310 311 /** 312 * Called from the throttle slot when the final write of throttle id has been 313 * completed, and the slot is set as initialized, or called directly for our own shared throttles. 314 * @param t the throttle 315 * @param s the lot. 316 */ 317 protected void notifyComplete(DccThrottle t, LocoNetSlot s) { 318 // end the waiting thread since we got a response 319 s.notifySlotListeners(); // make sure other listeners for this slot 320 // know about what's going on! 321 notifyThrottleKnown(t, new DccLocoAddress(s.locoAddr(), isLongAddress(s.locoAddr()))); 322 synchronized (this) { 323 if (waitingForNotification.containsKey(s.locoAddr())) { 324 log.debug( 325 "LnThrottleManager.notifyChangedSlot() - removing throttle acquisition notification flagging for address {}", 326 s.locoAddr()); 327 waitingForNotification.get(s.locoAddr()).interrupt(); 328 waitingForNotification.remove(s.locoAddr()); 329 } else { 330 log.debug( 331 "LnThrottleManager.notifyChangedSlot() - ignoring slot notification for slot {}, address {} account not attempting to acquire that address", 332 s.getSlot(), s.locoAddr()); 333 } 334 slotForAddress.remove(s.locoAddr()); 335 } 336 requestOutstanding = false; 337 pendingRequestAddress = null; 338 processQueuedThrottleSetupRequest(); 339 } 340 341 /** 342 * Loco acquisition failed. Propagate the failure message to the (GUI) 343 * throttle. 344 * 345 * @param address of the loco which could not be acquired 346 * @param cause reason for the failure 347 */ 348 public void notifyRefused(int address, String cause) { 349 //end the waiting thread since we got a failure response 350 synchronized (this) { 351 if (waitingForNotification.containsKey(address)) { 352 waitingForNotification.get(address).interrupt(); 353 waitingForNotification.remove(address); 354 // notify the throttle - in some other thread! 355 356 class InformRejection implements Runnable { 357 // inform the throttle from a new thread, so that 358 // the modal dialog box doesn't block other LocoNet 359 // message handling 360 361 final int address; 362 final String cause; 363 364 InformRejection(int address, String s) { 365 this.address = address; 366 this.cause = s; 367 } 368 369 @Override 370 public void run() { 371 372 log.debug("New thread launched to inform throttle user of failure to acquire loco {} - {}", address, cause); 373 failedThrottleRequest(new DccLocoAddress(address, isLongAddress(address)), cause); 374 } 375 376 } 377 Thread thr = new Thread(new InformRejection(address, cause)); 378 thr.start(); 379 } 380 slotForAddress.remove(address); 381 } 382 requestOutstanding = false; 383 pendingRequestAddress = null; 384 processQueuedThrottleSetupRequest(); 385 } 386 387 388 /** 389 * Create a LocoNet Throttle to control a loco. 390 * <p> 391 * This is called during the loco acquisition process by logic within 392 * LnThrottleManager. Generally, it should not be directly called by other 393 * methods. 394 * 395 * @param memo connection memo used by the throttle for communications 396 * @param s slot holding an acquired loco 397 * @return throttle holding an acquired loco 398 */ 399 DccThrottle createThrottle(LocoNetSystemConnectionMemo memo, LocoNetSlot s) { 400 log.debug("createThrottle: slot {}", s.getSlot()); 401 return new LocoNetThrottle(memo, s); 402 } 403 404 /** 405 * Determines if the loco address is a long address. 406 * <p> 407 * For LocoNet, address 128 and above is a long address. 408 * 409 * @param address to be checked 410 * @return true if long address, else false 411 */ 412 @Override 413 public boolean canBeLongAddress(int address) { 414 return isLongAddress(address); 415 } 416 417 /** 418 * Determines if the loco address is a short address. 419 * <p> 420 * For LocoNet, address 127 and below is a short address 421 * 422 * @param address to be checked 423 * @return true if short address, else false 424 */ 425 @Override 426 public boolean canBeShortAddress(int address) { 427 return !isLongAddress(address); 428 } 429 430 /** 431 * Reports whether all loco addresses are uniquely long or short, without any 432 * ambiguity for any address. 433 * <p> 434 * For LocoNet, there are no ambiguous addresses. 435 * 436 * @return true 437 */ 438 @Override 439 public boolean addressTypeUnique() { 440 return true; 441 } 442 443 /** 444 * Local method for deciding short/long address. 445 * 446 * @param num address to be checked 447 * @return true if num is a long address else false 448 */ 449 protected static boolean isLongAddress(int num) { 450 return (num >= 128); 451 } 452 453 /** 454 * Disposes a LnThrottle object. 455 * <p> 456 * Generally, this will cause the slot to be made "common" and the LnThrottle 457 * is disposed of. 458 * <p> 459 * After disposal, the throttle may not be used to control the loco. 460 * 461 * @param t is a throttle to be disposed of 462 * @param l is the listener for the throttle 463 * @return false if throttle is not a LocoNetThrottle, else true 464 */ 465 @Override 466 public boolean disposeThrottle(DccThrottle t, ThrottleListener l) { 467 log.debug("disposeThrottle - throttle {}", t.getLocoAddress()); 468 if (t instanceof LocoNetThrottle) { 469 if (super.disposeThrottle(t, l)) { 470 LocoNetThrottle lnt = (LocoNetThrottle) t; 471 lnt.throttleDispose(); 472 return true; 473 } 474 } 475 return false; 476 } 477 478 /** 479 * Dispatches a loco from a LnThrottle object. 480 * <p> 481 * Generally, this will cause the slot to be made "common" and then linked via 482 * the "Dispatch" slot. 483 * <p> 484 * After dispatching, the throttle may not be used to control the loco. 485 * You should check getUsageCountBefore calling as it will fail if not 1. 486 * 487 * @param t is a throttle to be disposed of 488 * @param l is the listener for the throttle 489 */ 490 @Override 491 public void dispatchThrottle(DccThrottle t, ThrottleListener l) { 492 log.debug("dispatchThrottle - throttle {}", t.getLocoAddress()); 493 // Use slot to dispatch, then release 494 if (t instanceof LocoNetThrottle) { 495 // only dispatch if its the last throttle use 496 if (super.getThrottleUsageCount(t.getLocoAddress()) == 1) { 497 ((LocoNetThrottle) t).dispatchThrottle(t, l); 498 } else { 499 return; 500 } 501 } 502 super.releaseThrottle(t, l); 503 } 504 505 /** 506 * Dispatch a loco from a LnThrottle object. 507 * <p> 508 * Generally, this will cause the slot to be made "common". 509 * <p> 510 * After disposal, the throttle may not be used to control the loco. 511 * 512 * @param t is a throttle to be disposed of 513 * @param l is the listener for the throttle 514 */ 515 @Override 516 public void releaseThrottle(DccThrottle t, ThrottleListener l) { 517 log.debug("releaseThrottle - throttle {}", t.getLocoAddress()); 518 super.releaseThrottle(t, l); 519 } 520 521 /** 522 * Cancels the loco acquisition process when throttle acquisition of a loco 523 * fails. 524 * 525 * @param address loco address which could not be acquired 526 * @param reason for the failure 527 */ 528 @Override 529 public void failedThrottleRequest(LocoAddress address, String reason) { 530 super.failedThrottleRequest(address, reason); 531 log.debug("failedThrottleRequest - address {}, reason {}", address, reason); 532 //now end and remove any waiting thread 533 synchronized (this) { 534 if (waitingForNotification.containsKey(address.getNumber())) { 535 waitingForNotification.get(address.getNumber()).interrupt(); 536 waitingForNotification.remove(address.getNumber()); 537 } 538 slotForAddress.remove(address.getNumber()); 539 } 540 requestOutstanding = false; 541 pendingRequestAddress = null; 542 processQueuedThrottleSetupRequest(); 543 } 544 545 /** 546 * Cancel a request for a throttle. 547 * 548 * @param address The decoder address desired. 549 * address. 550 * @param l The ThrottleListener cancelling request for a throttle. 551 */ 552 @Override 553 public void cancelThrottleRequest(LocoAddress address, ThrottleListener l) { 554 555 // calling super removes the ThrottleListener from the callback list, 556 // The listener which has just sent the cancel doesn't need notification 557 // of the cancel but other listeners might 558 super.cancelThrottleRequest(address, l); 559 560 failedThrottleRequest(address, "Throttle Request " + address + " Cancelled."); 561 562 int loconumber = address.getNumber(); 563 log.debug("cancelThrottleRequest - loconumber {}", loconumber); 564 synchronized (this) { 565 if (waitingForNotification.containsKey(loconumber)) { 566 waitingForNotification.get(loconumber).interrupt(); 567 waitingForNotification.remove(loconumber); 568 } 569 slotForAddress.remove(loconumber); 570 } 571 requestOutstanding = false; 572 pendingRequestAddress = null; 573 processQueuedThrottleSetupRequest(); 574 } 575 576 protected int throttleID = 0x0171; 577 578 /** 579 * Get the ThrottleID value for this throttle. 580 * 581 * @return the ThrottleID value 582 */ 583 public int getThrottleID() { 584 return throttleID; 585 } 586 587 /** 588 * {@inheritDoc} 589 * Dispose of this manager, typically for testing. 590 */ 591 @Override 592 public void dispose() { 593 if (retrySetupThread != null) { 594 try { 595 retrySetupThread.interrupt(); 596 retrySetupThread.join(); 597 } catch (InterruptedException ex) { 598 log.warn("dispose interrupted"); 599 } 600 } 601 } 602 603 /** 604 * Inform the requesting throttle object (not the connection-specific throttle 605 * implementation!) that the address is in-use and the throttle user may 606 * either choose to "steal" the address, or quit the acquisition process. 607 * The LocoNet acquisition process "retry" timer is stopped as part of this 608 * process, since a positive response has been received from the command station 609 * and since user intervention is required. 610 * 611 * Reminder: for LocoNet throttles which are not using "expanded slot" 612 * functionality, "steal" really means "share". For those LocoNet throttles 613 * which are using "expanded slots", "steal" really means take control and 614 * let the command station issue a "StealZap" LocoNet message to the other throttle. 615 * 616 * @param locoAddr address of DCC loco or consist 617 */ 618 public void notifyStealRequest(int locoAddr) { 619 // need to find the "throttleListener" associated with the request for locoAddr, and 620 // send that "throttleListener" a notification that the command station needs 621 // permission to "steal" the loco address. 622 synchronized (this) { 623 if (waitingForNotification.containsKey(locoAddr)) { 624 waitingForNotification.get(locoAddr).interrupt(); 625 waitingForNotification.remove(locoAddr); 626 627 notifyDecisionRequest(new DccLocoAddress(locoAddr, isLongAddress(locoAddr)), ThrottleListener.DecisionType.STEAL); 628 } 629 } 630 } 631 632 /** 633 * Perform the actual "Steal" of the requested throttle. 634 * <p> 635 * This is a call-back, as a result of the throttle user's agreement to 636 * "steal" the locomotive. 637 * <p> 638 * Reminder: for LocoNet throttles which are not using "expanded slot" 639 * functionality, "steal" really means "share". For those LocoNet throttles 640 * which are using "expanded slots", "steal" really means "force any other 641 * throttle running that address to drop the loco". 642 * 643 * @param address desired DccLocoAddress 644 * @param decision made by the ThrottleListener, only listening for STEAL 645 * @since 4.9.2 646 */ 647 @Override 648 public void responseThrottleDecision(LocoAddress address, ThrottleListener l, ThrottleListener.DecisionType decision) { 649 650 log.debug("{} decision invoked for address {}",decision,address.getNumber() ); 651 652 if ( decision == ThrottleListener.DecisionType.STEAL ) { 653 // Steal is currently implemented by using the same method 654 // we used to acquire the slot prior to the release of 655 // Digitrax command stations with expanded slots. 656 LocoNetSlot slot; 657 synchronized (this) { 658 slot = slotForAddress.get(address.getNumber()); 659 } 660 // Only continue if address is found in a slot 661 if (slot != null) { 662 slot.setIsInitialized(false); 663 commitToAcquireThrottle(slot); 664 } else { 665 log.error("Address {} not found in list of slots", address.getNumber()); 666 } 667 } else { 668 log.error("Invalid DecisionType {} for LnThrottleManager.",decision); 669 } 670 } 671 672 /* 673 * Internal class for holding throttleListener/LocoAddress pairs for 674 * outstanding requests. 675 */ 676 protected static class ThrottleRequest { 677 private LocoAddress la = null; 678 private boolean tc = false; 679 680 ThrottleRequest(LocoAddress l, boolean control) { 681 la = l; 682 tc = control; 683 } 684 685 public boolean getControl() { 686 return tc; 687 } 688 public LocoAddress getAddress() { 689 return la; 690 } 691 692 } 693 694 private static final Logger log = LoggerFactory.getLogger(LnThrottleManager.class); 695 696}