001package jmri.jmrix.can.cbus; 002 003import java.util.*; 004import java.util.concurrent.ConcurrentHashMap; 005import java.awt.GraphicsEnvironment; 006 007import jmri.*; 008import jmri.jmrit.throttle.ThrottlesPreferences; 009import jmri.jmrix.AbstractThrottleManager; 010import jmri.jmrix.can.*; 011import jmri.util.TimerUtil; 012import jmri.util.ThreadingUtil; 013import jmri.util.swing.JmriJOptionPane; 014 015import static jmri.ThrottleListener.DecisionType; 016 017/** 018 * CBUS implementation of a ThrottleManager. 019 * 020 * @author Bob Jacobsen Copyright (C) 2001 021 * @author Andrew Crosland Copyright (C) 2009 022 * @author Steve Young Copyright (C) 2019 023 * @author Andrew Crosland Copyright (C) 2021 024 */ 025public class CbusThrottleManager extends AbstractThrottleManager implements CanListener, Disposable { 026 027 private boolean _handleExpected = false; 028 private boolean _handleExpectedSecondLevelRequest = false; 029 private int _intAddr; 030 private DccLocoAddress _dccAddr; 031 protected int THROTTLE_TIMEOUT = 5000; 032 private boolean canErrorDialogVisible; 033 private boolean invalidErrorDialogVisible; 034 private boolean _singleThrottleInUse = false; // For single throttle support 035 036 private final ConcurrentHashMap<Integer, CbusThrottle> softThrottles = new ConcurrentHashMap<>(CbusConstants.CBUS_MAX_SLOTS); 037 038 public CbusThrottleManager(CanSystemConnectionMemo memo) { 039 super(memo); 040 this.memo = memo; 041 tc = memo.getTrafficController(); 042 addTc(tc); 043 } 044 045 /** 046 * {@inheritDoc} 047 */ 048 @Override 049 public void dispose() { 050 removeTc(tc); 051 stopThrottleRequestTimer(); 052 } 053 054 private final TrafficController tc; 055 private final CanSystemConnectionMemo memo; 056 057 /** 058 * CBUS allows Throttle sharing, both internally within JMRI and externally by command stations 059 * <p> 060 * {@inheritDoc} 061 */ 062 @Override 063 protected boolean singleUse() { 064 return false; 065 } 066 067 /** 068 * {@inheritDoc} 069 */ 070 @Override 071 public void requestThrottleSetup(LocoAddress address, boolean control) { 072 startThrottleRequestTimer(false); 073 requestThrottleSetup(address, DecisionType.STEAL_OR_SHARE); 074 } 075 076 /** 077 * As this method is called by both throttle recovery and normal throttle creation, 078 * methods calling need to start their own timeouts to ensure the correct 079 * error message is displayed. 080 */ 081 private void requestThrottleSetup(LocoAddress address, DecisionType decision) { 082 if ( !( address instanceof DccLocoAddress)) { 083 log.error("{} is not a DccLocoAddress",address); 084 return; 085 } 086 087 if (memo.hasMultipleThrottles() || !_singleThrottleInUse) { 088 _dccAddr = (DccLocoAddress) address; 089 _intAddr = _dccAddr.getNumber(); 090 091 // The CBUS protocol requires that we request a session from the command 092 // station. Throttle object will be notified by Command Station 093 log.debug("Requesting {} session for loco {}",decision,_dccAddr); 094 if (_dccAddr.isLongAddress()) { 095 _intAddr |= 0xC000; 096 } 097 CanMessage msg; 098 099 switch (decision) { 100 case STEAL_OR_SHARE: 101 // 1st line request 102 // Request a session for this throttle normally 103 _handleExpectedSecondLevelRequest = false; 104 msg = new CanMessage(3, tc.getCanid()); 105 msg.setOpCode(CbusConstants.CBUS_RLOC); 106 msg.setElement(1, _intAddr / 256); 107 msg.setElement(2, _intAddr & 0xff); 108 break; 109 case STEAL: 110 // 2nd line request 111 // Request a Steal session 112 _handleExpectedSecondLevelRequest = true; 113 msg = new CanMessage(4, tc.getCanid()); 114 msg.setOpCode(CbusConstants.CBUS_GLOC); 115 msg.setElement(1, _intAddr / 256); 116 msg.setElement(2, _intAddr & 0xff); 117 msg.setElement(3, 0x01); // bit 0 flag set 118 break; 119 case SHARE: 120 // 2nd line request 121 // Request a Share session 122 _handleExpectedSecondLevelRequest = true; 123 msg = new CanMessage(4, tc.getCanid()); 124 msg.setOpCode(CbusConstants.CBUS_GLOC); 125 msg.setElement(1, _intAddr / 256); 126 msg.setElement(2, _intAddr & 0xff); 127 msg.setElement(3, 0x02); // bit 1 flag set 128 break; 129 default: 130 log.error("decision type {} unknown to CbusThrottleManager",decision); 131 return; 132 } 133 134 // send the request to layout 135 _handleExpected = true; 136 tc.sendCanMessage(msg, this); 137 } else { 138 failedThrottleRequest(address, "Only one Throttle can be in use at anyone time with this connection."); 139 log.warn("Single CBUS Throttle already in use"); 140 } 141 } 142 143 /** 144 * stopAll() 145 * 146 * <p> 147 * Called when track stopped message received. Sets all JMRI managed 148 * throttles to speed zero 149 */ 150 private void stopAll() { 151 // Get set of handles for JMRI managed throttles and 152 // iterate over them setting the speed of each throttle to 0 153 // log.info("stopAll() setting all speeds to emergency stop"); 154 for (Map.Entry<Integer, CbusThrottle> entry : softThrottles.entrySet()) { 155 CbusThrottle throttle = entry.getValue(); 156 throttle.setSpeedSetting(-1.0f); 157 } 158 } 159 160 /** 161 * {@inheritDoc} 162 */ 163 @Override 164 public void message(CanMessage m) { 165 if ( m.extendedOrRtr() ) { 166 return; 167 } 168 int opc = m.getElement(0); 169 int handle; 170 switch (opc) { 171 case CbusConstants.CBUS_ESTOP: 172 case CbusConstants.CBUS_RESTP: 173 stopAll(); 174 break; 175 case CbusConstants.CBUS_KLOC: // Kill loco 176 log.debug("Kill loco message"); 177 // Find a throttle corresponding to the handle 178 handle = m.getElement(1); 179 for (Map.Entry<Integer, CbusThrottle> entry : softThrottles.entrySet()) { 180 CbusThrottle throttle = entry.getValue(); 181 if (throttle.getHandle() == handle) { 182 // stop Throttle from sending keep-alives 183 throttle.throttleDispose(); 184 // remove from abstract list 185 forceDisposeThrottle(throttle.getLocoAddress()); 186 // Remove the Throttle from the managed list 187 softThrottles.remove(throttle.getHandle()); 188 } 189 } 190 _singleThrottleInUse = false; 191 break; 192 case CbusConstants.CBUS_DSPD: 193 // only if emergency stop 194 if ((m.getElement(2) & 0x7f) == 1) { 195 // Find a throttle corresponding to the handle 196 handle = m.getElement(1); 197 for (Map.Entry<Integer, CbusThrottle> entry : softThrottles.entrySet()) { 198 CbusThrottle throttle = entry.getValue(); 199 if (throttle.getHandle() == handle) { 200 // Set the throttle session to match the DSPD packet 201 throttle.updateSpeedSetting(m.getElement(2) & 0x7f); 202 throttle.updateIsForward((m.getElement(2) & 0x80) == 0x80); 203 } 204 } 205 } 206 break; 207 default: 208 break; 209 } 210 } 211 212 /** 213 * {@inheritDoc} 214 */ 215 @edu.umd.cs.findbugs.annotations.SuppressFBWarnings({"SLF4J_SIGN_ONLY_FORMAT", "SLF4J_FORMAT_SHOULD_BE_CONST"}) 216 // justification="I18N of log message") 217 @Override 218 public void reply(CanReply m) { 219 if ( m.extendedOrRtr() ) { 220 return; 221 } 222 int opc = m.getElement(0); 223 int handle = m.getElement(1); 224 225 switch (opc) { 226 case CbusConstants.CBUS_PLOC: 227 int rcvdIntAddr = (m.getElement(2) & 0x3f) * 256 + m.getElement(3); 228 boolean rcvdIsLong = (m.getElement(2) & 0xc0) != 0; 229 DccLocoAddress rcvdDccAddr = new DccLocoAddress(rcvdIntAddr, rcvdIsLong); 230 log.debug("Throttle manager received PLOC with session {} for address {}",m.getElement(1),rcvdIntAddr); 231 if ((_handleExpected) && rcvdDccAddr.equals(_dccAddr)) { 232 log.debug("PLOC was expected"); 233 // We're expecting an engine report and it matches our address 234 stopThrottleRequestTimer(); 235 handle = m.getElement(1); 236 if (!memo.hasMultipleThrottles()) { 237 _singleThrottleInUse = true; 238 } 239 240 // check if the PLOC has come from a throttle session cancel notification 241 for (Map.Entry<Integer, CbusThrottle> entry : softThrottles.entrySet()) { 242 CbusThrottle throttle = entry.getValue(); 243 if (throttle.isStolen()) { 244 log.debug("setting handle from {} to {}",throttle.getHandle(),handle); 245 throttle.setHandle(handle); 246 // uses timeout to help prevent steal loops 247 // jmri.util.ThreadingUtil.runOnLayoutDelayed( () -> { 248 throttle.setStolen(false); // sends the reactivation PCL 249 // },500 ); 250 throttle.throttleInit(m.getElement(4), m.getElement(5), m.getElement(6), m.getElement(7)); 251 _handleExpected = false; 252 return; 253 } 254 } 255 256 // Initialise new throttle from PLOC data to allow taking over moving trains 257 CbusThrottle throttle = new CbusThrottle((CanSystemConnectionMemo) adapterMemo, rcvdDccAddr, handle); 258 notifyThrottleKnown(throttle, rcvdDccAddr); 259 throttle.throttleInit(m.getElement(4), m.getElement(5), m.getElement(6), m.getElement(7)); 260 softThrottles.put(handle, throttle); 261 _handleExpected = false; 262 } 263 break; 264 case CbusConstants.CBUS_ERR: 265 handleErr(m); 266 break; 267 case CbusConstants.CBUS_DSPD: 268 // Find a throttle corresponding to the handle 269 for (Map.Entry<Integer, CbusThrottle> entry : softThrottles.entrySet()) { 270 CbusThrottle throttle = entry.getValue(); 271 if (throttle.getHandle() == handle) { 272 // Set the throttle session to match the DSPD packet received 273 throttle.updateSpeedSetting(m.getElement(2) & 0x7f); 274 throttle.updateIsForward((m.getElement(2) & 0x80) == 0x80); 275 // if something external to JMRI is sharing a session 276 // dispatch is invalid 277 throttle.setDispatchActive(false); 278 } 279 } 280 break; 281 282 case CbusConstants.CBUS_DFUN: 283 // Find a throttle corresponding to the handle 284 for (Map.Entry<Integer, CbusThrottle> entry : softThrottles.entrySet()) { 285 CbusThrottle throttle = entry.getValue(); 286 if (throttle.getHandle() == handle) { 287 // if something external to JMRI is sharing a session 288 // dispatch is invalid 289 throttle.setDispatchActive(false); 290 throttle.updateFunctionGroup(m.getElement(2),m.getElement(3)); 291 } 292 } 293 break; 294 295 case CbusConstants.CBUS_DFNON: 296 case CbusConstants.CBUS_DFNOF: 297 // Find a throttle corresponding to the handle 298 for (Map.Entry<Integer, CbusThrottle> entry : softThrottles.entrySet()) { 299 CbusThrottle throttle = entry.getValue(); 300 if (throttle.getHandle() == handle) { 301 // dispatch is invalid if something external to JMRI is sharing a session 302 throttle.setDispatchActive(false); 303 throttle.updateFunction(m.getElement(2), (opc == CbusConstants.CBUS_DFNON)); 304 } 305 } 306 break; 307 308 case CbusConstants.CBUS_ESTOP: 309 case CbusConstants.CBUS_RESTP: 310 stopAll(); 311 break; 312 case CbusConstants.CBUS_DKEEP: 313 for (Map.Entry<Integer, CbusThrottle> entry : softThrottles.entrySet()) { 314 CbusThrottle throttle = entry.getValue(); 315 if (throttle.getHandle() == handle) { 316 // if something external to JMRI is sharing a session 317 // dispatch is invalid 318 throttle.setDispatchActive(false); 319 } 320 } 321 break; 322 default: 323 break; 324 } 325 } 326 327 @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value="SLF4J_SIGN_ONLY_FORMAT", 328 justification="I18N of log message") 329 private void handleErr(CanReply m) { 330 int handle = m.getElement(1); 331 int rcvdIntAddr = (m.getElement(1) & 0x3f) * 256 + m.getElement(2); 332 boolean rcvdIsLong = (m.getElement(1) & 0xc0) != 0; 333 // DccLocoAddress rcvdDccAddr = new DccLocoAddress(rcvdIntAddr, rcvdIsLong); 334 int errCode = m.getElement(3); 335 336 boolean responseForUs = ((_handleExpected) && new DccLocoAddress(rcvdIntAddr, rcvdIsLong).equals(_dccAddr)); 337 338 switch (errCode) { 339 case CbusConstants.ERR_LOCO_STACK_FULL: 340 case CbusConstants.ERR_LOCO_ADDRESS_TAKEN: 341 342 String errStr; 343 if ( errCode == CbusConstants.ERR_LOCO_STACK_FULL ){ 344 errStr = Bundle.getMessage("ERR_LOCO_STACK_FULL") + " " + rcvdIntAddr; 345 } else { 346 errStr = Bundle.getMessage("ERR_LOCO_ADDRESS_TAKEN", rcvdIntAddr); 347 } 348 349 // log.debug("handlexpected {} _dccAddr {} got {} ", _handleExpected, _dccAddr, rcvdDccAddr); 350 351 if (responseForUs) { // We were expecting an engine report and it matches our address 352 log.debug("Failed throttle request due to ERR"); 353 _handleExpected = false; 354 stopThrottleRequestTimer(); 355 356 // if this is the result of a share or steal request, 357 // we need to stop here and inform the ThrottleListener 358 if ( _handleExpectedSecondLevelRequest ){ 359 failedThrottleRequest(_dccAddr, errStr); 360 return; 361 } 362 363 // so this is the message from the 1st normal request 364 // now we check the command station, 365 // and notify the ThrottleListener () 366 367 boolean steal = false; 368 boolean share = false; 369 370 CbusCommandStation cs = (CbusCommandStation) memo.get(CommandStation.class); 371 372 if ( cs != null ) { 373 log.debug("cs says can steal {}, can share {}", cs.isStealAvailable(), cs.isShareAvailable() ); 374 steal = cs.isStealAvailable(); 375 share = cs.isShareAvailable(); 376 } 377 378 if ( !steal && !share ){ 379 failedThrottleRequest(_dccAddr, errStr); 380 } 381 else if ( steal && share ){ 382 notifyDecisionRequest(_dccAddr, DecisionType.STEAL_OR_SHARE); 383 } 384 else if ( steal ){ 385 notifyDecisionRequest(_dccAddr, DecisionType.STEAL); 386 } 387 else { // must be share 388 notifyDecisionRequest(_dccAddr, DecisionType.SHARE); 389 } 390 } else { 391 log.debug("ERR address not matched"); 392 } 393 break; 394 395 case CbusConstants.ERR_SESSION_NOT_PRESENT: 396 // most likely called via a command station being reset or 397 // coming back online 398 log.warn("{}",Bundle.getMessage("ERR_SESSION_NOT_PRESENT",handle)); 399 400 if (responseForUs) { 401 // We were expecting an engine report and it matches our address 402 _handleExpected = false; 403 failedThrottleRequest(_dccAddr, Bundle.getMessage("CBUS_ERROR") 404 + Bundle.getMessage("ERR_SESSION_NOT_PRESENT",handle)); 405 log.warn("Session not present when expecting a session number"); 406 } 407 408 // check if it's a JMRI throttle session, 409 // Inform the throttle associated with this session handle, if any 410 for (Map.Entry<Integer, CbusThrottle> entry : softThrottles.entrySet()) { 411 CbusThrottle throttle = entry.getValue(); 412 if (throttle.getHandle() == handle) { 413 log.warn("Cancelling JMRI Throttle Session {} for loco {}", 414 handle, 415 throttle.getLocoAddress().toString() 416 ); 417 attemptRecoverThrottle(throttle); 418 break; 419 } 420 } 421 break; 422 case CbusConstants.ERR_CONSIST_EMPTY: 423 log.warn("{} {}",Bundle.getMessage("ERR_CONSIST_EMPTY"), handle); 424 break; 425 case CbusConstants.ERR_LOCO_NOT_FOUND: 426 log.warn("{} {}", Bundle.getMessage("ERR_LOCO_NOT_FOUND"), handle); 427 break; 428 case CbusConstants.ERR_CAN_BUS_ERROR: 429 log.error("{}",Bundle.getMessage("ERR_CAN_BUS_ERROR")); 430 if (!GraphicsEnvironment.isHeadless() && !canErrorDialogVisible ) { 431 canErrorDialogVisible = true; 432 ThreadingUtil.runOnGUI(() -> 433 JmriJOptionPane.showMessageDialogNonModal(null, // parent 434 Bundle.getMessage("ERR_CAN_BUS_ERROR"), // message 435 Bundle.getMessage("CBUS_ERROR"), // title 436 JmriJOptionPane.ERROR_MESSAGE, // message type 437 () -> canErrorDialogVisible = false )); // callback 438 } 439 return; 440 case CbusConstants.ERR_INVALID_REQUEST: 441 log.error("{}", Bundle.getMessage("ERR_INVALID_REQUEST")); 442 if (!GraphicsEnvironment.isHeadless() && !invalidErrorDialogVisible){ 443 invalidErrorDialogVisible = true; 444 ThreadingUtil.runOnGUI(() -> 445 JmriJOptionPane.showMessageDialogNonModal(null, // parent 446 Bundle.getMessage("ERR_INVALID_REQUEST"), // message 447 Bundle.getMessage("CBUS_ERROR"), // title 448 JmriJOptionPane.ERROR_MESSAGE, // message type 449 () -> invalidErrorDialogVisible = false )); // callback 450 } 451 return; 452 case CbusConstants.ERR_SESSION_CANCELLED: 453 // There will be a session cancelled error for the other throttle(s) 454 // when you are stealing, but as you don't yet have a session id, it 455 // won't match so you will ignore it, then a PLOC will come with that 456 // session id and your requested loco number which is giving it to you. 457 458 log.debug("{}", Bundle.getMessage("ERR_SESSION_CANCELLED",handle)); 459 460 // Inform the throttle associated with this session handle, if any 461 for (Map.Entry<Integer, CbusThrottle> entry : softThrottles.entrySet()) { 462 CbusThrottle throttle = entry.getValue(); 463 if (throttle.getHandle() == handle) { 464 if (throttle.isStolen()){ // already actioned 465 log.debug("external steal already actioned, returning"); 466 return; 467 } 468 log.warn("External Steal / Cancel for loco {} Session {} ",throttle.getLocoAddress(), handle ); 469 attemptRecoverThrottle(throttle); 470 break; 471 } 472 } 473 break; 474 default: 475 log.error("{} error code: {}", Bundle.getMessage("ERR_UNKNOWN"), errCode); 476 break; 477 } 478 } 479 480 /** 481 * Attempts Throttle Recovery when a session has been lost 482 */ 483 private void attemptRecoverThrottle(CbusThrottle throttle){ 484 485 log.debug("start of recovery, current throttle stolen {} session {} num recovr attempts {} hashmap size {}", 486 throttle.isStolen(), throttle.getHandle(), throttle.getNumRecoverAttempts(), 487 softThrottles.size() ); 488 489 int oldhandle = throttle.getHandle(); 490 491 throttle.increaseNumRecoverAttempts(); 492 493 if (throttle.getNumRecoverAttempts() > 10) { // catch runaways 494 _handleExpected = false; 495 throttle.throttleDispose(); // stop throttle keep-alive messages, send PCL ThrottleConnected false 496 showSessionCancelDialogue(throttle.getLocoAddress()); 497 softThrottles.remove(oldhandle); // remove from local list 498 forceDisposeThrottle( throttle.getLocoAddress() ); // remove from JMRI share list 499 } 500 501 throttle.setStolen(true); 502 throttle.setHandle(-1); 503 504 boolean steal = false; 505 boolean share = false; 506 507 CbusCommandStation cs = (CbusCommandStation) memo.get(CommandStation.class); 508 if ( cs != null ) { 509 log.debug("cs says can steal {}, can share {}", cs.isStealAvailable(), cs.isShareAvailable() ); 510 steal = cs.isStealAvailable(); 511 share = cs.isShareAvailable(); 512 } 513 514 if (share && InstanceManager.getDefault(ThrottlesPreferences.class).isSilentShare()){ 515 // share is available on command station AND silent share preference checked 516 log.info("Requesting Silent Share loco {} to regain a session",throttle.getLocoAddress()); 517 ThreadingUtil.runOnLayoutDelayed( () -> { 518 startThrottleRequestTimer(true); 519 requestThrottleSetup(throttle.getLocoAddress(), DecisionType.SHARE); 520 },1000); 521 } 522 else if (steal && InstanceManager.getDefault(ThrottlesPreferences.class).isSilentSteal()) { 523 // steal is available on command station AND silent steal preference checked 524 log.info("Requesting Silent Steal loco {} to regain a session",throttle.getLocoAddress()); 525 ThreadingUtil.runOnLayoutDelayed( () -> { 526 startThrottleRequestTimer(true); 527 requestThrottleSetup(throttle.getLocoAddress(), DecisionType.STEAL); 528 },1000); 529 } else { 530 throttle.throttleDispose(); // stop throttle keep-alive messages, send PCL ThrottleConnected false 531 showSessionCancelDialogue(throttle.getLocoAddress()); 532 softThrottles.remove(oldhandle); // remove from local list 533 forceDisposeThrottle( throttle.getLocoAddress() ); // remove from JMRI share list 534 } 535 } 536 537 /** 538 * CBUS has a dynamic Dispatch function, defaulting to false 539 * {@inheritDoc} 540 */ 541 @Override 542 public boolean hasDispatchFunction() { 543 return false; 544 } 545 546 /** 547 * Any address is potentially a long address. 548 * {@inheritDoc} 549 */ 550 @Override 551 public boolean canBeLongAddress(int address) { 552 return address > 0; 553 } 554 555 /** 556 * Address 127 and below is a short address. 557 * {@inheritDoc} 558 */ 559 @Override 560 public boolean canBeShortAddress(int address) { 561 return address < 128; 562 } 563 564 /** 565 * Short and long address spaces overlap and are not unique. 566 * @return always false. 567 * {@inheritDoc} 568 */ 569 @Override 570 public boolean addressTypeUnique() { 571 return false; 572 } 573 574 /** 575 * Local method for deciding short/long address. 576 * @param num the address number 577 * @return true if address equal to or greater than 128. 578 */ 579 static boolean isLongAddress(int num) { 580 return (num >= 128); 581 } 582 583 /** 584 * Hardware has a stealing implementation. 585 * {@inheritDoc} 586 */ 587 @Override 588 public boolean enablePrefSilentStealOption() { 589 return true; 590 } 591 592 /** 593 * Hardware has a sharing implementation. 594 * {@inheritDoc} 595 */ 596 @Override 597 public boolean enablePrefSilentShareOption() { 598 return true; 599 } 600 601 /** 602 * CBUS Hardware will make its own decision on preferred option. 603 * <p> 604 * This is the default for scripts that do NOT have a GUI for asking what to do when 605 * a steal / share decision is required. 606 * {@inheritDoc} 607 */ 608 @Override 609 protected void makeHardwareDecision(LocoAddress address,DecisionType question){ 610 // no need to check if share / steal currently enabled on command station, 611 // this has already been done to produce the correct question 612 switch (question) { 613 case STEAL: 614 // share has been disabled in command station 615 responseThrottleDecision(address, null, DecisionType.STEAL ); 616 break; 617 case SHARE: 618 // steal has been disabled in command station 619 responseThrottleDecision(address, null, DecisionType.SHARE ); 620 break; 621 default: // case STEAL_OR_SHARE: 622 if ( InstanceManager.getDefault(ThrottlesPreferences.class).isSilentSteal() ){ 623 responseThrottleDecision(address, null, DecisionType.STEAL ); 624 } 625 else { 626 responseThrottleDecision(address, null, DecisionType.SHARE ); 627 } 628 break; 629 } 630 } 631 632 /** 633 * Send a request to steal or share a requested throttle. 634 * <p> 635 * {@inheritDoc} 636 * 637 */ 638 @Override 639 public void responseThrottleDecision(LocoAddress address, ThrottleListener l, DecisionType decision) { 640 log.debug("Received {} response for Loco {}, listener {}",decision,address,l); 641 startThrottleRequestTimer(false); 642 requestThrottleSetup(address,decision); 643 } 644 645 private TimerTask throttleRequestTimer; 646 647 /** 648 * Start timer to wait for command station to respond to RLOC or GLOC 649 */ 650 private void startThrottleRequestTimer(boolean isRecovery) { 651 throttleRequestTimer = new TimerTask() { 652 @Override 653 public void run() { 654 timeout(isRecovery); 655 } 656 }; 657 TimerUtil.schedule(throttleRequestTimer, ( THROTTLE_TIMEOUT ) ); 658 } 659 660 private void stopThrottleRequestTimer(){ 661 if (throttleRequestTimer!=null){ 662 throttleRequestTimer.cancel(); 663 } 664 throttleRequestTimer = null; 665 } 666 667 /** 668 * Internal routine to notify failed throttle request a timeout 669 */ 670 private void timeout(boolean isRecovery) { 671 log.debug("Throttle request (RLOC or PLOC) timed out"); 672 stopThrottleRequestTimer(); 673 if (isRecovery){ 674 log.warn("Session recovery not possible for {}",_dccAddr); 675 forceDisposeThrottle( _dccAddr ); // remove from JMRI share list 676 677 for (Map.Entry<Integer, CbusThrottle> entry : softThrottles.entrySet()) { 678 CbusThrottle throttle = entry.getValue(); 679 if (throttle.getLocoAddress() == _dccAddr) { 680 throttle.throttleDispose(); 681 showSessionCancelDialogue(_dccAddr); 682 softThrottles.remove(throttle.getHandle()); 683 } 684 } 685 } 686 else { // not in recovery, normal request timeout, is command station connected? 687 failedThrottleRequest(_dccAddr, Bundle.getMessage("ERR_THROTTLE_TIMEOUT")); 688 } 689 } 690 691 /** 692 * MERG CBUS Throttle sessions default to 128 SS. 693 * This can be changed by a subsequent message from Throttle to CS, 694 * or by message from Command Station to CbusThrottle. 695 * Supported modes are 128, 28 and 14. 696 * {@inheritDoc } 697 */ 698 @Override 699 public EnumSet<SpeedStepMode> supportedSpeedModes() { 700 return EnumSet.of(SpeedStepMode.NMRA_DCC_128 701 , SpeedStepMode.NMRA_DCC_28 702 , SpeedStepMode.NMRA_DCC_14); 703 } 704 705 /** 706 * {@inheritDoc} 707 */ 708 @Override 709 public boolean disposeThrottle(DccThrottle t, ThrottleListener l) { 710 log.debug("disposeThrottle called for {}", t); 711 if (t instanceof CbusThrottle) { 712 log.debug("Cbus Dispose calling abstract Throttle manager dispose"); 713 if (super.disposeThrottle(t, l)) { 714 715 CbusThrottle lnt = (CbusThrottle) t; 716 lnt.releaseFromCommandStation(); 717 lnt.throttleDispose(); 718 // forceDisposeThrottle( (DccLocoAddress) lnt.getLocoAddress() ); 719 log.debug("called throttleDispose"); 720 _singleThrottleInUse = false; 721 return true; 722 } 723 } 724 return false; 725 } 726 727 /** 728 * {@inheritDoc} 729 */ 730 @Override 731 protected void forceDisposeThrottle(LocoAddress la) { 732 super.forceDisposeThrottle(la); 733 _singleThrottleInUse = false; 734 } 735 736 /** 737 * {@inheritDoc} 738 */ 739 @Override 740 protected void updateNumUsers( LocoAddress la, int numUsers ){ 741 log.debug("throttle {} notification that num. users is now {}",la,numUsers); 742 for (Map.Entry<Integer, CbusThrottle> entry : softThrottles.entrySet()) { 743 CbusThrottle throttle = entry.getValue(); 744 if (throttle.getLocoAddress() == la) { 745 if ((numUsers == 1) && throttle.getSpeedSetting() > 0) { 746 throttle.setDispatchActive(true); 747 return; 748 } 749 throttle.setDispatchActive(false); 750 } 751 } 752 } 753 754 /** 755 * {@inheritDoc} 756 */ 757 @Override 758 public void cancelThrottleRequest(LocoAddress address, ThrottleListener l) { 759 760 // calling super removes the ThrottleListener from the callback list, 761 // The listener which has just sent the cancel doesn't need notification 762 // of the cancel but other listeners might 763 super.cancelThrottleRequest(address, l); 764 failedThrottleRequest(address, "Throttle Request " + address + " Cancelled."); 765 } 766 767 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(CbusThrottleManager.class); 768 769}