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