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}