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