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}