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 @Override 216 public void reply(CanReply m) { 217 if ( m.extendedOrRtr() ) { 218 return; 219 } 220 int opc = m.getElement(0); 221 int handle = m.getElement(1); 222 223 switch (opc) { 224 case CbusConstants.CBUS_PLOC: 225 int rcvdIntAddr = (m.getElement(2) & 0x3f) * 256 + m.getElement(3); 226 boolean rcvdIsLong = (m.getElement(2) & 0xc0) != 0; 227 DccLocoAddress rcvdDccAddr = new DccLocoAddress(rcvdIntAddr, rcvdIsLong); 228 log.debug("Throttle manager received PLOC with session {} for address {}",m.getElement(1),rcvdIntAddr); 229 if ((_handleExpected) && rcvdDccAddr.equals(_dccAddr)) { 230 log.debug("PLOC was expected"); 231 // We're expecting an engine report and it matches our address 232 stopThrottleRequestTimer(); 233 handle = m.getElement(1); 234 if (!memo.hasMultipleThrottles()) { 235 _singleThrottleInUse = true; 236 } 237 238 // check if the PLOC has come from a throttle session cancel notification 239 for (Map.Entry<Integer, CbusThrottle> entry : softThrottles.entrySet()) { 240 CbusThrottle throttle = entry.getValue(); 241 if (throttle.isStolen()) { 242 log.debug("setting handle from {} to {}",throttle.getHandle(),handle); 243 throttle.setHandle(handle); 244 // uses timeout to help prevent steal loops 245 // jmri.util.ThreadingUtil.runOnLayoutDelayed( () -> { 246 throttle.setStolen(false); // sends the reactivation PCL 247 // },500 ); 248 throttle.throttleInit(m.getElement(4), m.getElement(5), m.getElement(6), m.getElement(7)); 249 _handleExpected = false; 250 return; 251 } 252 } 253 254 // Initialise new throttle from PLOC data to allow taking over moving trains 255 CbusThrottle throttle = new CbusThrottle((CanSystemConnectionMemo) adapterMemo, rcvdDccAddr, handle); 256 notifyThrottleKnown(throttle, rcvdDccAddr); 257 throttle.throttleInit(m.getElement(4), m.getElement(5), m.getElement(6), m.getElement(7)); 258 softThrottles.put(handle, throttle); 259 _handleExpected = false; 260 } 261 break; 262 case CbusConstants.CBUS_ERR: 263 handleErr(m); 264 break; 265 case CbusConstants.CBUS_DSPD: 266 // Find a throttle corresponding to the handle 267 for (Map.Entry<Integer, CbusThrottle> entry : softThrottles.entrySet()) { 268 CbusThrottle throttle = entry.getValue(); 269 if (throttle.getHandle() == handle) { 270 // Set the throttle session to match the DSPD packet received 271 throttle.updateSpeedSetting(m.getElement(2) & 0x7f); 272 throttle.updateIsForward((m.getElement(2) & 0x80) == 0x80); 273 // if something external to JMRI is sharing a session 274 // dispatch is invalid 275 throttle.setDispatchActive(false); 276 } 277 } 278 break; 279 280 case CbusConstants.CBUS_DFUN: 281 // Find a throttle corresponding to the handle 282 for (Map.Entry<Integer, CbusThrottle> entry : softThrottles.entrySet()) { 283 CbusThrottle throttle = entry.getValue(); 284 if (throttle.getHandle() == handle) { 285 // if something external to JMRI is sharing a session 286 // dispatch is invalid 287 throttle.setDispatchActive(false); 288 throttle.updateFunctionGroup(m.getElement(2),m.getElement(3)); 289 } 290 } 291 break; 292 293 case CbusConstants.CBUS_DFNON: 294 case CbusConstants.CBUS_DFNOF: 295 // Find a throttle corresponding to the handle 296 for (Map.Entry<Integer, CbusThrottle> entry : softThrottles.entrySet()) { 297 CbusThrottle throttle = entry.getValue(); 298 if (throttle.getHandle() == handle) { 299 // dispatch is invalid if something external to JMRI is sharing a session 300 throttle.setDispatchActive(false); 301 throttle.updateFunction(m.getElement(2), (opc == CbusConstants.CBUS_DFNON)); 302 } 303 } 304 break; 305 306 case CbusConstants.CBUS_ESTOP: 307 case CbusConstants.CBUS_RESTP: 308 stopAll(); 309 break; 310 case CbusConstants.CBUS_DKEEP: 311 for (Map.Entry<Integer, CbusThrottle> entry : softThrottles.entrySet()) { 312 CbusThrottle throttle = entry.getValue(); 313 if (throttle.getHandle() == handle) { 314 // if something external to JMRI is sharing a session 315 // dispatch is invalid 316 throttle.setDispatchActive(false); 317 } 318 } 319 break; 320 default: 321 break; 322 } 323 } 324 325 @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value="SLF4J_SIGN_ONLY_FORMAT", 326 justification="I18N of log message") 327 private void handleErr(CanReply m) { 328 int handle = m.getElement(1); 329 int rcvdIntAddr = (m.getElement(1) & 0x3f) * 256 + m.getElement(2); 330 boolean rcvdIsLong = (m.getElement(1) & 0xc0) != 0; 331 // DccLocoAddress rcvdDccAddr = new DccLocoAddress(rcvdIntAddr, rcvdIsLong); 332 int errCode = m.getElement(3); 333 334 boolean responseForUs = ((_handleExpected) && new DccLocoAddress(rcvdIntAddr, rcvdIsLong).equals(_dccAddr)); 335 336 switch (errCode) { 337 case CbusConstants.ERR_LOCO_STACK_FULL: 338 case CbusConstants.ERR_LOCO_ADDRESS_TAKEN: 339 340 String errStr; 341 if ( errCode == CbusConstants.ERR_LOCO_STACK_FULL ){ 342 errStr = Bundle.getMessage("ERR_LOCO_STACK_FULL") + " " + rcvdIntAddr; 343 } else { 344 errStr = Bundle.getMessage("ERR_LOCO_ADDRESS_TAKEN", rcvdIntAddr); 345 } 346 347 // log.debug("handlexpected {} _dccAddr {} got {} ", _handleExpected, _dccAddr, rcvdDccAddr); 348 349 if (responseForUs) { // We were expecting an engine report and it matches our address 350 log.debug("Failed throttle request due to ERR"); 351 _handleExpected = false; 352 stopThrottleRequestTimer(); 353 354 // if this is the result of a share or steal request, 355 // we need to stop here and inform the ThrottleListener 356 if ( _handleExpectedSecondLevelRequest ){ 357 failedThrottleRequest(_dccAddr, errStr); 358 return; 359 } 360 361 // so this is the message from the 1st normal request 362 // now we check the command station, 363 // and notify the ThrottleListener () 364 365 boolean steal = false; 366 boolean share = false; 367 368 CbusCommandStation cs = (CbusCommandStation) memo.get(CommandStation.class); 369 370 if ( cs != null ) { 371 log.debug("cs says can steal {}, can share {}", cs.isStealAvailable(), cs.isShareAvailable() ); 372 steal = cs.isStealAvailable(); 373 share = cs.isShareAvailable(); 374 } 375 376 if ( !steal && !share ){ 377 failedThrottleRequest(_dccAddr, errStr); 378 } 379 else if ( steal && share ){ 380 notifyDecisionRequest(_dccAddr, DecisionType.STEAL_OR_SHARE); 381 } 382 else if ( steal ){ 383 notifyDecisionRequest(_dccAddr, DecisionType.STEAL); 384 } 385 else { // must be share 386 notifyDecisionRequest(_dccAddr, DecisionType.SHARE); 387 } 388 } else { 389 log.debug("ERR address not matched"); 390 } 391 break; 392 393 case CbusConstants.ERR_SESSION_NOT_PRESENT: 394 // most likely called via a command station being reset or 395 // coming back online 396 log.warn("{}",Bundle.getMessage("ERR_SESSION_NOT_PRESENT",handle)); 397 398 if (responseForUs) { 399 // We were expecting an engine report and it matches our address 400 _handleExpected = false; 401 failedThrottleRequest(_dccAddr, Bundle.getMessage("CBUS_ERROR") 402 + Bundle.getMessage("ERR_SESSION_NOT_PRESENT",handle)); 403 log.warn("Session not present when expecting a session number"); 404 } 405 406 // check if it's a JMRI throttle session, 407 // Inform the throttle associated with this session handle, if any 408 for (Map.Entry<Integer, CbusThrottle> entry : softThrottles.entrySet()) { 409 CbusThrottle throttle = entry.getValue(); 410 if (throttle.getHandle() == handle) { 411 log.warn("Cancelling JMRI Throttle Session {} for loco {}", 412 handle, 413 throttle.getLocoAddress().toString() 414 ); 415 attemptRecoverThrottle(throttle); 416 break; 417 } 418 } 419 break; 420 case CbusConstants.ERR_CONSIST_EMPTY: 421 log.warn("{} {}",Bundle.getMessage("ERR_CONSIST_EMPTY"), handle); 422 break; 423 case CbusConstants.ERR_LOCO_NOT_FOUND: 424 log.warn("{} {}", Bundle.getMessage("ERR_LOCO_NOT_FOUND"), handle); 425 break; 426 case CbusConstants.ERR_CAN_BUS_ERROR: 427 log.error("{}",Bundle.getMessage("ERR_CAN_BUS_ERROR")); 428 if (!GraphicsEnvironment.isHeadless() && !canErrorDialogVisible ) { 429 canErrorDialogVisible = true; 430 ThreadingUtil.runOnGUI(() -> 431 JmriJOptionPane.showMessageDialogNonModal(null, // parent 432 Bundle.getMessage("ERR_CAN_BUS_ERROR"), // message 433 Bundle.getMessage("CBUS_ERROR"), // title 434 JmriJOptionPane.ERROR_MESSAGE, // message type 435 () -> canErrorDialogVisible = false )); // callback 436 } 437 return; 438 case CbusConstants.ERR_INVALID_REQUEST: 439 log.error("{}", Bundle.getMessage("ERR_INVALID_REQUEST")); 440 if (!GraphicsEnvironment.isHeadless() && !invalidErrorDialogVisible){ 441 invalidErrorDialogVisible = true; 442 ThreadingUtil.runOnGUI(() -> 443 JmriJOptionPane.showMessageDialogNonModal(null, // parent 444 Bundle.getMessage("ERR_INVALID_REQUEST"), // message 445 Bundle.getMessage("CBUS_ERROR"), // title 446 JmriJOptionPane.ERROR_MESSAGE, // message type 447 () -> invalidErrorDialogVisible = false )); // callback 448 } 449 return; 450 case CbusConstants.ERR_SESSION_CANCELLED: 451 // There will be a session cancelled error for the other throttle(s) 452 // when you are stealing, but as you don't yet have a session id, it 453 // won't match so you will ignore it, then a PLOC will come with that 454 // session id and your requested loco number which is giving it to you. 455 456 log.debug("{}", Bundle.getMessage("ERR_SESSION_CANCELLED",handle)); 457 458 // Inform the throttle associated with this session handle, if any 459 for (Map.Entry<Integer, CbusThrottle> entry : softThrottles.entrySet()) { 460 CbusThrottle throttle = entry.getValue(); 461 if (throttle.getHandle() == handle) { 462 if (throttle.isStolen()){ // already actioned 463 log.debug("external steal already actioned, returning"); 464 return; 465 } 466 log.warn("External Steal / Cancel for loco {} Session {} ",throttle.getLocoAddress(), handle ); 467 attemptRecoverThrottle(throttle); 468 break; 469 } 470 } 471 break; 472 default: 473 log.error("{} error code: {}", Bundle.getMessage("ERR_UNKNOWN"), errCode); 474 break; 475 } 476 } 477 478 /** 479 * Attempts Throttle Recovery when a session has been lost 480 */ 481 private void attemptRecoverThrottle(CbusThrottle throttle){ 482 483 log.debug("start of recovery, current throttle stolen {} session {} num recovr attempts {} hashmap size {}", 484 throttle.isStolen(), throttle.getHandle(), throttle.getNumRecoverAttempts(), 485 softThrottles.size() ); 486 487 int oldhandle = throttle.getHandle(); 488 489 throttle.increaseNumRecoverAttempts(); 490 491 if (throttle.getNumRecoverAttempts() > 10) { // catch runaways 492 _handleExpected = false; 493 throttle.throttleDispose(); // stop throttle keep-alive messages, send PCL ThrottleConnected false 494 showSessionCancelDialogue(throttle.getLocoAddress()); 495 softThrottles.remove(oldhandle); // remove from local list 496 forceDisposeThrottle( throttle.getLocoAddress() ); // remove from JMRI share list 497 } 498 499 throttle.setStolen(true); 500 throttle.setHandle(-1); 501 502 boolean steal = false; 503 boolean share = false; 504 505 CbusCommandStation cs = (CbusCommandStation) memo.get(CommandStation.class); 506 if ( cs != null ) { 507 log.debug("cs says can steal {}, can share {}", cs.isStealAvailable(), cs.isShareAvailable() ); 508 steal = cs.isStealAvailable(); 509 share = cs.isShareAvailable(); 510 } 511 512 if (share && InstanceManager.getDefault(ThrottlesPreferences.class).isSilentShare()){ 513 // share is available on command station AND silent share preference checked 514 log.info("Requesting Silent Share loco {} to regain a session",throttle.getLocoAddress()); 515 ThreadingUtil.runOnLayoutDelayed( () -> { 516 startThrottleRequestTimer(true); 517 requestThrottleSetup(throttle.getLocoAddress(), DecisionType.SHARE); 518 },1000); 519 } 520 else if (steal && InstanceManager.getDefault(ThrottlesPreferences.class).isSilentSteal()) { 521 // steal is available on command station AND silent steal preference checked 522 log.info("Requesting Silent Steal loco {} to regain a session",throttle.getLocoAddress()); 523 ThreadingUtil.runOnLayoutDelayed( () -> { 524 startThrottleRequestTimer(true); 525 requestThrottleSetup(throttle.getLocoAddress(), DecisionType.STEAL); 526 },1000); 527 } else { 528 throttle.throttleDispose(); // stop throttle keep-alive messages, send PCL ThrottleConnected false 529 showSessionCancelDialogue(throttle.getLocoAddress()); 530 softThrottles.remove(oldhandle); // remove from local list 531 forceDisposeThrottle( throttle.getLocoAddress() ); // remove from JMRI share list 532 } 533 } 534 535 /** 536 * CBUS has a dynamic Dispatch function, defaulting to false 537 * {@inheritDoc} 538 */ 539 @Override 540 public boolean hasDispatchFunction() { 541 return false; 542 } 543 544 /** 545 * Any address is potentially a long address. 546 * {@inheritDoc} 547 */ 548 @Override 549 public boolean canBeLongAddress(int address) { 550 return address > 0; 551 } 552 553 /** 554 * Address 127 and below is a short address. 555 * {@inheritDoc} 556 */ 557 @Override 558 public boolean canBeShortAddress(int address) { 559 return address < 128; 560 } 561 562 /** 563 * Short and long address spaces overlap and are not unique. 564 * @return always false. 565 * {@inheritDoc} 566 */ 567 @Override 568 public boolean addressTypeUnique() { 569 return false; 570 } 571 572 /** 573 * Local method for deciding short/long address. 574 * @param num the address number 575 * @return true if address equal to or greater than 128. 576 */ 577 static boolean isLongAddress(int num) { 578 return (num >= 128); 579 } 580 581 /** 582 * Hardware has a stealing implementation. 583 * {@inheritDoc} 584 */ 585 @Override 586 public boolean enablePrefSilentStealOption() { 587 return true; 588 } 589 590 /** 591 * Hardware has a sharing implementation. 592 * {@inheritDoc} 593 */ 594 @Override 595 public boolean enablePrefSilentShareOption() { 596 return true; 597 } 598 599 /** 600 * CBUS Hardware will make its own decision on preferred option. 601 * <p> 602 * This is the default for scripts that do NOT have a GUI for asking what to do when 603 * a steal / share decision is required. 604 * {@inheritDoc} 605 */ 606 @Override 607 protected void makeHardwareDecision(LocoAddress address,DecisionType question){ 608 // no need to check if share / steal currently enabled on command station, 609 // this has already been done to produce the correct question 610 switch (question) { 611 case STEAL: 612 // share has been disabled in command station 613 responseThrottleDecision(address, null, DecisionType.STEAL ); 614 break; 615 case SHARE: 616 // steal has been disabled in command station 617 responseThrottleDecision(address, null, DecisionType.SHARE ); 618 break; 619 default: // case STEAL_OR_SHARE: 620 if ( InstanceManager.getDefault(ThrottlesPreferences.class).isSilentSteal() ){ 621 responseThrottleDecision(address, null, DecisionType.STEAL ); 622 } 623 else { 624 responseThrottleDecision(address, null, DecisionType.SHARE ); 625 } 626 break; 627 } 628 } 629 630 /** 631 * Send a request to steal or share a requested throttle. 632 * <p> 633 * {@inheritDoc} 634 * 635 */ 636 @Override 637 public void responseThrottleDecision(LocoAddress address, ThrottleListener l, DecisionType decision) { 638 log.debug("Received {} response for Loco {}, listener {}",decision,address,l); 639 startThrottleRequestTimer(false); 640 requestThrottleSetup(address,decision); 641 } 642 643 private TimerTask throttleRequestTimer; 644 645 /** 646 * Start timer to wait for command station to respond to RLOC or GLOC 647 */ 648 private void startThrottleRequestTimer(boolean isRecovery) { 649 throttleRequestTimer = new TimerTask() { 650 @Override 651 public void run() { 652 timeout(isRecovery); 653 } 654 }; 655 TimerUtil.schedule(throttleRequestTimer, ( THROTTLE_TIMEOUT ) ); 656 } 657 658 private void stopThrottleRequestTimer(){ 659 if (throttleRequestTimer!=null){ 660 throttleRequestTimer.cancel(); 661 } 662 throttleRequestTimer = null; 663 } 664 665 /** 666 * Internal routine to notify failed throttle request a timeout 667 */ 668 private void timeout(boolean isRecovery) { 669 log.debug("Throttle request (RLOC or PLOC) timed out"); 670 stopThrottleRequestTimer(); 671 if (isRecovery){ 672 log.warn("Session recovery not possible for {}",_dccAddr); 673 forceDisposeThrottle( _dccAddr ); // remove from JMRI share list 674 675 for (Map.Entry<Integer, CbusThrottle> entry : softThrottles.entrySet()) { 676 CbusThrottle throttle = entry.getValue(); 677 if (throttle.getLocoAddress() == _dccAddr) { 678 throttle.throttleDispose(); 679 showSessionCancelDialogue(_dccAddr); 680 softThrottles.remove(throttle.getHandle()); 681 } 682 } 683 } 684 else { // not in recovery, normal request timeout, is command station connected? 685 failedThrottleRequest(_dccAddr, Bundle.getMessage("ERR_THROTTLE_TIMEOUT")); 686 } 687 } 688 689 /** 690 * MERG CBUS Throttle sessions default to 128 SS. 691 * This can be changed by a subsequent message from Throttle to CS, 692 * or by message from Command Station to CbusThrottle. 693 * Supported modes are 128, 28 and 14. 694 * {@inheritDoc } 695 */ 696 @Override 697 public EnumSet<SpeedStepMode> supportedSpeedModes() { 698 return EnumSet.of(SpeedStepMode.NMRA_DCC_128 699 , SpeedStepMode.NMRA_DCC_28 700 , SpeedStepMode.NMRA_DCC_14); 701 } 702 703 /** 704 * {@inheritDoc} 705 */ 706 @Override 707 public boolean disposeThrottle(DccThrottle t, ThrottleListener l) { 708 log.debug("disposeThrottle called for {}", t); 709 if (t instanceof CbusThrottle) { 710 log.debug("Cbus Dispose calling abstract Throttle manager dispose"); 711 if (super.disposeThrottle(t, l)) { 712 713 CbusThrottle lnt = (CbusThrottle) t; 714 lnt.releaseFromCommandStation(); 715 lnt.throttleDispose(); 716 // forceDisposeThrottle( (DccLocoAddress) lnt.getLocoAddress() ); 717 log.debug("called throttleDispose"); 718 _singleThrottleInUse = false; 719 return true; 720 } 721 } 722 return false; 723 } 724 725 /** 726 * {@inheritDoc} 727 */ 728 @Override 729 protected void forceDisposeThrottle(LocoAddress la) { 730 super.forceDisposeThrottle(la); 731 _singleThrottleInUse = false; 732 } 733 734 /** 735 * {@inheritDoc} 736 */ 737 @Override 738 protected void updateNumUsers( LocoAddress la, int numUsers ){ 739 log.debug("throttle {} notification that num. users is now {}",la,numUsers); 740 for (Map.Entry<Integer, CbusThrottle> entry : softThrottles.entrySet()) { 741 CbusThrottle throttle = entry.getValue(); 742 if (throttle.getLocoAddress() == la) { 743 if ((numUsers == 1) && throttle.getSpeedSetting() > 0) { 744 throttle.setDispatchActive(true); 745 return; 746 } 747 throttle.setDispatchActive(false); 748 } 749 } 750 } 751 752 /** 753 * {@inheritDoc} 754 */ 755 @Override 756 public void cancelThrottleRequest(LocoAddress address, ThrottleListener l) { 757 758 // calling super removes the ThrottleListener from the callback list, 759 // The listener which has just sent the cancel doesn't need notification 760 // of the cancel but other listeners might 761 super.cancelThrottleRequest(address, l); 762 failedThrottleRequest(address, "Throttle Request " + address + " Cancelled."); 763 } 764 765 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(CbusThrottleManager.class); 766 767}