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 if (requestOutstanding) { 079 try { 080 // queue this request for later. 081 requestList.put(new ThrottleRequest(address,control)); 082 } catch (InterruptedException ie) { 083 log.error("Interrupted while trying to store throttle request"); 084 requestOutstanding = false; 085 } 086 } else { 087 // handle this now 088 requestOutstanding = true; 089 processThrottleSetupRequest(address, control); 090 } 091 } 092 093 /** 094 * Processes the next loco from the queue of requested locos for which to get 095 * a LocoNetThrottle. 096 */ 097 protected void processQueuedThrottleSetupRequest() { 098 if (!requestOutstanding && (requestList.size() != 0 )) { 099 requestOutstanding = true; 100 try { 101 ThrottleRequest tr = requestList.take(); 102 processThrottleSetupRequest(tr.getAddress(), tr.getControl()); 103 } catch (InterruptedException ie) { 104 log.error("Interrupted while trying to process process throttle request"); 105 requestOutstanding = false; 106 } 107 } 108 } 109 110 /** 111 * Begin the processing of a Throttle Request. 112 * 113 * @param address Loco address 114 * @param control whether the throttle object wants to control the loco 115 */ 116 private void processThrottleSetupRequest(LocoAddress address, boolean control) { 117 slotManager.slotFromLocoAddress(address.getNumber(), this); //first try 118 119 class RetrySetup implements Runnable { // setup for retries and failure check 120 121 final DccLocoAddress address; 122 final SlotListener list; 123 124 RetrySetup(DccLocoAddress address, SlotListener list) { 125 this.address = address; 126 this.list = list; 127 } 128 129 @Override 130 public void run() { 131 int attempts = 1; // already tried once above 132 int maxAttempts = 10; 133 while (attempts <= maxAttempts) { 134 try { 135 Thread.sleep(1000); // wait one second 136 } catch (InterruptedException ex) { 137 return; // stop waiting if slot is found or error occurs 138 } 139 String again = ""; 140 if (attempts < maxAttempts) { 141 slotManager.slotFromLocoAddress(address.getNumber(), list); 142 again = ", trying again."; // NOI18N 143 } 144 log.debug("No response to slot request for {}, attempt {} {}", address, attempts, again); 145 attempts++; 146 } 147 log.error("No response to slot request for {} after {} attempts.", address, attempts - 1); // NOI18N 148 failedThrottleRequest(address, "Failed to get response from command station"); 149 requestOutstanding = false; 150 processQueuedThrottleSetupRequest(); 151 } 152 } 153 154 retrySetupThread = new Thread( 155 new RetrySetup(new DccLocoAddress(address.getNumber(), 156 isLongAddress(address.getNumber())), this)); 157 retrySetupThread.setName("LnThrottleManager RetrySetup " + address); 158 retrySetupThread.start(); 159 synchronized (this) { 160 waitingForNotification.put(address.getNumber(), retrySetupThread); 161 } 162 } 163 164 volatile Thread retrySetupThread; 165 166 Hashtable<Integer, Thread> waitingForNotification = new Hashtable<>(5); 167 168 Hashtable<Integer, LocoNetSlot> slotForAddress; 169 LinkedBlockingQueue<ThrottleRequest> requestList; 170 boolean requestOutstanding = false; 171 172 /** 173 * LocoNet does have a Dispatch function. 174 * 175 * @return true 176 */ 177 @Override 178 public boolean hasDispatchFunction() { 179 return true; 180 } 181 182 /** 183 * What speed modes are supported by this system? value should be xor of 184 * possible modes specified by the DccThrottle interface. 185 * 186 * @return an integer containing the combined speed step modes supported 187 */ 188 @Override 189 public EnumSet<SpeedStepMode> supportedSpeedModes() { 190 return EnumSet.of(SpeedStepMode.NMRA_DCC_128 191 , SpeedStepMode.NMRA_DCC_28 192 , SpeedStepMode.MOTOROLA_28 193 , SpeedStepMode.NMRA_DCC_14); 194 } 195 196 /** 197 * Get notification that an address has changed slot. This method creates a 198 * throttle for all ThrottleListeners of that address and notifies them via 199 * the ThrottleListener.notifyThrottleFound method. 200 * 201 * @param s LocoNet slot which has been changed 202 */ 203 @Override 204 public void notifyChangedSlot(LocoNetSlot s) { 205 log.debug("notifyChangedSlot - slot {}, slotStatus {}", s.getSlot(), Integer.toHexString(s.slotStatus())); 206 // This is invoked only if the SlotManager knows that the LnThrottleManager is 207 // interested in the address associated with this slot. 208 209 // need to check to see if the slot is in a suitable state for creating a throttle. 210 if (s.slotStatus() == LnConstants.LOCO_IN_USE) { 211 // loco is already in-use 212 log.warn("slot {} address {} is already in-use.", 213 s.getSlot(), s.locoAddr()); 214 // is the throttle ID the same as for this JMRI instance? If not, do not accept the slot. 215 if ((s.id() != 0) && s.id() != throttleID) { 216 // notify the LnThrottleManager about failure of acquisition. 217 // NEED TO TRIGGER THE NEW "STEAL REQUIRED" FUNCTIONALITY HERE 218 //note: throttle listener expects to have "callback" method notifyDecisionRequired 219 //invoked if a "steal" is required. Make that happen as part of the "acquisition" process 220 synchronized (this) { 221 slotForAddress.put(s.locoAddr(), s); 222 } 223 notifyStealRequest(s.locoAddr()); 224 return; 225 } 226 // shared throttle / already ours 227 notifyComplete(commitToAcquireThrottle(s),s); 228 return; 229 } 230 commitToAcquireThrottle(s); 231 } 232 233 /** 234 * Making progress in the process of acquiring a throttle. 235 * 236 * @param s slot to be acquired 237 */ 238 private DccThrottle commitToAcquireThrottle(LocoNetSlot s) { 239 // haven't identified a particular reason to refuse throttle acquisition at this time... 240 return createThrottle((LocoNetSystemConnectionMemo) adapterMemo, s); 241 // the rest is done when the write of the throttle ID has been acknowledged in the throttle 242 // by calling notifyComplete 243 } 244 245 /** 246 * Called from the throttle slot when the final write of throttle id has been 247 * completed, and the slot is set as initialized, or called directly for our own shared throttles. 248 * @param t the throttle 249 * @param s the lot. 250 */ 251 protected void notifyComplete(DccThrottle t, LocoNetSlot s) { 252 // end the waiting thread since we got a response 253 s.notifySlotListeners(); // make sure other listeners for this slot 254 // know about what's going on! 255 notifyThrottleKnown(t, new DccLocoAddress(s.locoAddr(), isLongAddress(s.locoAddr()))); 256 synchronized (this) { 257 if (waitingForNotification.containsKey(s.locoAddr())) { 258 log.debug( 259 "LnThrottleManager.notifyChangedSlot() - removing throttle acquisition notification flagging for address {}", 260 s.locoAddr()); 261 waitingForNotification.get(s.locoAddr()).interrupt(); 262 waitingForNotification.remove(s.locoAddr()); 263 } else { 264 log.debug( 265 "LnThrottleManager.notifyChangedSlot() - ignoring slot notification for slot {}, address {} account not attempting to acquire that address", 266 s.getSlot(), s.locoAddr()); 267 } 268 slotForAddress.remove(s.locoAddr()); 269 } 270 requestOutstanding = false; 271 processQueuedThrottleSetupRequest(); 272 } 273 274 /** 275 * Loco acquisition failed. Propagate the failure message to the (GUI) 276 * throttle. 277 * 278 * @param address of the loco which could not be acquired 279 * @param cause reason for the failure 280 */ 281 public void notifyRefused(int address, String cause) { 282 //end the waiting thread since we got a failure response 283 synchronized (this) { 284 if (waitingForNotification.containsKey(address)) { 285 waitingForNotification.get(address).interrupt(); 286 waitingForNotification.remove(address); 287 // notify the throttle - in some other thread! 288 289 class InformRejection implements Runnable { 290 // inform the throttle from a new thread, so that 291 // the modal dialog box doesn't block other LocoNet 292 // message handling 293 294 final int address; 295 final String cause; 296 297 InformRejection(int address, String s) { 298 this.address = address; 299 this.cause = s; 300 } 301 302 @Override 303 public void run() { 304 305 log.debug("New thread launched to inform throttle user of failure to acquire loco {} - {}", address, cause); 306 failedThrottleRequest(new DccLocoAddress(address, isLongAddress(address)), cause); 307 } 308 309 } 310 Thread thr = new Thread(new InformRejection(address, cause)); 311 thr.start(); 312 } 313 slotForAddress.remove(address); 314 } 315 requestOutstanding = false; 316 processQueuedThrottleSetupRequest(); 317 } 318 319 320 /** 321 * Create a LocoNet Throttle to control a loco. 322 * <p> 323 * This is called during the loco acquisition process by logic within 324 * LnThrottleManager. Generally, it should not be directly called by other 325 * methods. 326 * 327 * @param memo connection memo used by the throttle for communications 328 * @param s slot holding an acquired loco 329 * @return throttle holding an acquired loco 330 */ 331 DccThrottle createThrottle(LocoNetSystemConnectionMemo memo, LocoNetSlot s) { 332 log.debug("createThrottle: slot {}", s.getSlot()); 333 return new LocoNetThrottle(memo, s); 334 } 335 336 /** 337 * Determines if the loco address is a long address. 338 * <p> 339 * For LocoNet, address 128 and above is a long address. 340 * 341 * @param address to be checked 342 * @return true if long address, else false 343 */ 344 @Override 345 public boolean canBeLongAddress(int address) { 346 return isLongAddress(address); 347 } 348 349 /** 350 * Determines if the loco address is a short address. 351 * <p> 352 * For LocoNet, address 127 and below is a short address 353 * 354 * @param address to be checked 355 * @return true if short address, else false 356 */ 357 @Override 358 public boolean canBeShortAddress(int address) { 359 return !isLongAddress(address); 360 } 361 362 /** 363 * Reports whether all loco addresses are uniquely long or short, without any 364 * ambiguity for any address. 365 * <p> 366 * For LocoNet, there are no ambiguous addresses. 367 * 368 * @return true 369 */ 370 @Override 371 public boolean addressTypeUnique() { 372 return true; 373 } 374 375 /** 376 * Local method for deciding short/long address. 377 * 378 * @param num address to be checked 379 * @return true if num is a long address else false 380 */ 381 protected static boolean isLongAddress(int num) { 382 return (num >= 128); 383 } 384 385 /** 386 * Disposes a LnThrottle object. 387 * <p> 388 * Generally, this will cause the slot to be made "common" and the LnThrottle 389 * is disposed of. 390 * <p> 391 * After disposal, the throttle may not be used to control the loco. 392 * 393 * @param t is a throttle to be disposed of 394 * @param l is the listener for the throttle 395 * @return false if throttle is not a LocoNetThrottle, else true 396 */ 397 @Override 398 public boolean disposeThrottle(DccThrottle t, ThrottleListener l) { 399 log.debug("disposeThrottle - throttle {}", t.getLocoAddress()); 400 if (t instanceof LocoNetThrottle) { 401 if (super.disposeThrottle(t, l)) { 402 LocoNetThrottle lnt = (LocoNetThrottle) t; 403 lnt.throttleDispose(); 404 return true; 405 } 406 } 407 return false; 408 } 409 410 /** 411 * Dispatches a loco from a LnThrottle object. 412 * <p> 413 * Generally, this will cause the slot to be made "common" and then linked via 414 * the "Dispatch" slot. 415 * <p> 416 * After dispatching, the throttle may not be used to control the loco. 417 * You should check getUsageCountBefore calling as it will fail if not 1. 418 * 419 * @param t is a throttle to be disposed of 420 * @param l is the listener for the throttle 421 */ 422 @Override 423 public void dispatchThrottle(DccThrottle t, ThrottleListener l) { 424 log.debug("dispatchThrottle - throttle {}", t.getLocoAddress()); 425 // Use slot to dispatch, then release 426 if (t instanceof LocoNetThrottle) { 427 // only dispatch if its the last throttle use 428 if (super.getThrottleUsageCount(t.getLocoAddress()) == 1) { 429 ((LocoNetThrottle) t).dispatchThrottle(t, l); 430 } else { 431 return; 432 } 433 } 434 super.releaseThrottle(t, l); 435 } 436 437 /** 438 * Dispatch a loco from a LnThrottle object. 439 * <p> 440 * Generally, this will cause the slot to be made "common". 441 * <p> 442 * After disposal, the throttle may not be used to control the loco. 443 * 444 * @param t is a throttle to be disposed of 445 * @param l is the listener for the throttle 446 */ 447 @Override 448 public void releaseThrottle(DccThrottle t, ThrottleListener l) { 449 log.debug("releaseThrottle - throttle {}", t.getLocoAddress()); 450 super.releaseThrottle(t, l); 451 } 452 453 /** 454 * Cancels the loco acquisition process when throttle acquisition of a loco 455 * fails. 456 * 457 * @param address loco address which could not be acquired 458 * @param reason for the failure 459 */ 460 @Override 461 public void failedThrottleRequest(LocoAddress address, String reason) { 462 super.failedThrottleRequest(address, reason); 463 log.debug("failedThrottleRequest - address {}, reason {}", address, reason); 464 //now end and remove any waiting thread 465 synchronized (this) { 466 if (waitingForNotification.containsKey(address.getNumber())) { 467 waitingForNotification.get(address.getNumber()).interrupt(); 468 waitingForNotification.remove(address.getNumber()); 469 } 470 slotForAddress.remove(address.getNumber()); 471 } 472 requestOutstanding = false; 473 processQueuedThrottleSetupRequest(); 474 } 475 476 /** 477 * Cancel a request for a throttle. 478 * 479 * @param address The decoder address desired. 480 * address. 481 * @param l The ThrottleListener cancelling request for a throttle. 482 */ 483 @Override 484 public void cancelThrottleRequest(LocoAddress address, ThrottleListener l) { 485 486 // calling super removes the ThrottleListener from the callback list, 487 // The listener which has just sent the cancel doesn't need notification 488 // of the cancel but other listeners might 489 super.cancelThrottleRequest(address, l); 490 491 failedThrottleRequest(address, "Throttle Request " + address + " Cancelled."); 492 493 int loconumber = address.getNumber(); 494 log.debug("cancelThrottleRequest - loconumber {}", loconumber); 495 synchronized (this) { 496 if (waitingForNotification.containsKey(loconumber)) { 497 waitingForNotification.get(loconumber).interrupt(); 498 waitingForNotification.remove(loconumber); 499 } 500 slotForAddress.remove(loconumber); 501 } 502 requestOutstanding = false; 503 processQueuedThrottleSetupRequest(); 504 } 505 506 protected int throttleID = 0x0171; 507 508 /** 509 * Get the ThrottleID value for this throttle. 510 * 511 * @return the ThrottleID value 512 */ 513 public int getThrottleID() { 514 return throttleID; 515 } 516 517 /** 518 * {@inheritDoc} 519 * Dispose of this manager, typically for testing. 520 */ 521 @Override 522 public void dispose() { 523 if (retrySetupThread != null) { 524 try { 525 retrySetupThread.interrupt(); 526 retrySetupThread.join(); 527 } catch (InterruptedException ex) { 528 log.warn("dispose interrupted"); 529 } 530 } 531 } 532 533 /** 534 * Inform the requesting throttle object (not the connection-specific throttle 535 * implementation!) that the address is in-use and the throttle user may 536 * either choose to "steal" the address, or quit the acquisition process. 537 * The LocoNet acquisition process "retry" timer is stopped as part of this 538 * process, since a positive response has been received from the command station 539 * and since user intervention is required. 540 * 541 * Reminder: for LocoNet throttles which are not using "expanded slot" 542 * functionality, "steal" really means "share". For those LocoNet throttles 543 * which are using "expanded slots", "steal" really means take control and 544 * let the command station issue a "StealZap" LocoNet message to the other throttle. 545 * 546 * @param locoAddr address of DCC loco or consist 547 */ 548 public void notifyStealRequest(int locoAddr) { 549 // need to find the "throttleListener" associated with the request for locoAddr, and 550 // send that "throttleListener" a notification that the command station needs 551 // permission to "steal" the loco address. 552 synchronized (this) { 553 if (waitingForNotification.containsKey(locoAddr)) { 554 waitingForNotification.get(locoAddr).interrupt(); 555 waitingForNotification.remove(locoAddr); 556 557 notifyDecisionRequest(new DccLocoAddress(locoAddr, isLongAddress(locoAddr)), ThrottleListener.DecisionType.STEAL); 558 } 559 } 560 } 561 562 /** 563 * Perform the actual "Steal" of the requested throttle. 564 * <p> 565 * This is a call-back, as a result of the throttle user's agreement to 566 * "steal" the locomotive. 567 * <p> 568 * Reminder: for LocoNet throttles which are not using "expanded slot" 569 * functionality, "steal" really means "share". For those LocoNet throttles 570 * which are using "expanded slots", "steal" really means "force any other 571 * throttle running that address to drop the loco". 572 * 573 * @param address desired DccLocoAddress 574 * @param decision made by the ThrottleListener, only listening for STEAL 575 * @since 4.9.2 576 */ 577 @Override 578 public void responseThrottleDecision(LocoAddress address, ThrottleListener l, ThrottleListener.DecisionType decision) { 579 580 log.debug("{} decision invoked for address {}",decision,address.getNumber() ); 581 582 if ( decision == ThrottleListener.DecisionType.STEAL ) { 583 // Steal is currently implemented by using the same method 584 // we used to acquire the slot prior to the release of 585 // Digitrax command stations with expanded slots. 586 LocoNetSlot slot; 587 synchronized (this) { 588 slot = slotForAddress.get(address.getNumber()); 589 } 590 // Only continue if address is found in a slot 591 if (slot != null) { 592 slot.setIsInitialized(false); 593 commitToAcquireThrottle(slot); 594 } else { 595 log.error("Address {} not found in list of slots", address.getNumber()); 596 } 597 } else { 598 log.error("Invalid DecisionType {} for LnThrottleManager.",decision); 599 } 600 } 601 602 /* 603 * Internal class for holding throttleListener/LocoAddress pairs for 604 * outstanding requests. 605 */ 606 protected static class ThrottleRequest { 607 private LocoAddress la = null; 608 private boolean tc = false; 609 610 ThrottleRequest(LocoAddress l, boolean control) { 611 la = l; 612 tc = control; 613 } 614 615 public boolean getControl() { 616 return tc; 617 } 618 public LocoAddress getAddress() { 619 return la; 620 } 621 622 } 623 624 private final static Logger log = LoggerFactory.getLogger(LnThrottleManager.class); 625 626}