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