001package jmri.jmrix.can.cbus;
002
003import java.util.*;
004import java.util.concurrent.ConcurrentHashMap;
005import java.awt.GraphicsEnvironment;
006
007import jmri.*;
008import jmri.jmrit.throttle.preferences.ThrottlesPreferences;
009import jmri.jmrix.AbstractThrottleManager;
010import jmri.jmrix.can.*;
011import jmri.util.TimerUtil;
012import jmri.util.ThreadingUtil;
013import jmri.util.swing.JmriJOptionPane;
014
015import static jmri.ThrottleListener.DecisionType;
016
017/**
018 * CBUS implementation of a ThrottleManager.
019 *
020 * @author Bob Jacobsen Copyright (C) 2001
021 * @author Andrew Crosland Copyright (C) 2009
022 * @author Steve Young Copyright (C) 2019
023 * @author Andrew Crosland Copyright (C) 2021
024 */
025public class CbusThrottleManager extends AbstractThrottleManager implements CanListener, Disposable {
026
027    private boolean _handleExpected = false;
028    private boolean _handleExpectedSecondLevelRequest = false;
029    private int _intAddr;
030    private DccLocoAddress _dccAddr;
031    protected int THROTTLE_TIMEOUT = 5000;
032    private boolean canErrorDialogVisible;
033    private boolean invalidErrorDialogVisible;
034    private boolean _singleThrottleInUse = false; // For single throttle support
035
036    private final ConcurrentHashMap<Integer, CbusThrottle> softThrottles = new ConcurrentHashMap<>(CbusConstants.CBUS_MAX_SLOTS);
037
038    public CbusThrottleManager(CanSystemConnectionMemo memo) {
039        super(memo);
040        this.memo = memo;
041        tc = memo.getTrafficController();
042        addTc(tc);
043    }
044
045    /**
046     * {@inheritDoc}
047     */
048    @Override
049    public void dispose() {
050        removeTc(tc);
051        stopThrottleRequestTimer();
052    }
053
054    private final TrafficController tc;
055    private final CanSystemConnectionMemo memo;
056
057    /**
058     * CBUS allows Throttle sharing, both internally within JMRI and externally by command stations
059     * <p>
060     * {@inheritDoc}
061     */
062    @Override
063    protected boolean singleUse() {
064        return false;
065    }
066
067    /**
068     * {@inheritDoc}
069     */
070    @Override
071    public void requestThrottleSetup(LocoAddress address, boolean control) {
072        startThrottleRequestTimer(false);
073        requestThrottleSetup(address, DecisionType.STEAL_OR_SHARE);
074    }
075
076    /**
077     * As this method is called by both throttle recovery and normal throttle creation,
078     * methods calling need to start their own timeouts to ensure the correct
079     * error message is displayed.
080     */
081    private void requestThrottleSetup(LocoAddress address, DecisionType decision) {
082        if ( !( address instanceof DccLocoAddress)) {
083            log.error("{} is not a DccLocoAddress",address);
084            return;
085        }
086
087        if (memo.hasMultipleThrottles() || !_singleThrottleInUse) {
088            _dccAddr = (DccLocoAddress) address;
089            _intAddr = _dccAddr.getNumber();
090
091            // The CBUS protocol requires that we request a session from the command
092            // station. Throttle object will be notified by Command Station
093            log.debug("Requesting {} session for loco {}",decision,_dccAddr);
094            if (_dccAddr.isLongAddress()) {
095                _intAddr |= 0xC000;
096            }
097            CanMessage msg;
098
099            switch (decision) {
100                case STEAL_OR_SHARE:
101                    // 1st line request
102                    // Request a session for this throttle normally
103                    _handleExpectedSecondLevelRequest = false;
104                    msg = new CanMessage(3, tc.getCanid());
105                    msg.setOpCode(CbusConstants.CBUS_RLOC);
106                    msg.setElement(1, _intAddr / 256);
107                    msg.setElement(2, _intAddr & 0xff);
108                    break;
109                case STEAL:
110                    // 2nd line request
111                    // Request a Steal session
112                    _handleExpectedSecondLevelRequest = true;
113                    msg = new CanMessage(4, tc.getCanid());
114                    msg.setOpCode(CbusConstants.CBUS_GLOC);
115                    msg.setElement(1, _intAddr / 256);
116                    msg.setElement(2, _intAddr & 0xff);
117                    msg.setElement(3, 0x01); // bit 0 flag set
118                    break;
119                case SHARE:
120                    // 2nd line request
121                    // Request a Share session
122                    _handleExpectedSecondLevelRequest = true;
123                    msg = new CanMessage(4, tc.getCanid());
124                    msg.setOpCode(CbusConstants.CBUS_GLOC);
125                    msg.setElement(1, _intAddr / 256);
126                    msg.setElement(2, _intAddr & 0xff);
127                    msg.setElement(3, 0x02); // bit 1 flag set
128                    break;
129                default:
130                    log.error("decision type {} unknown to CbusThrottleManager",decision);
131                    return;
132            }
133
134            // send the request to layout
135            _handleExpected = true;
136            tc.sendCanMessage(msg, this);
137        } else {
138            failedThrottleRequest(address, "Only one Throttle can be in use at anyone time with this connection.");
139            log.warn("Single CBUS Throttle already in use");
140        }
141    }
142
143    /**
144     * stopAll()
145     *
146     * <p>
147     * Called when track stopped message received. Sets all JMRI managed
148     * throttles to speed zero
149     */
150    private void stopAll() {
151        // Get set of handles for JMRI managed throttles and
152        // iterate over them setting the speed of each throttle to 0
153        // log.info("stopAll() setting all speeds to emergency stop");
154        for (Map.Entry<Integer, CbusThrottle> entry : softThrottles.entrySet()) {
155            CbusThrottle throttle = entry.getValue();
156            throttle.setSpeedSetting(-1.0f);
157        }
158    }
159
160    /**
161     * {@inheritDoc}
162     */
163    @Override
164    public void message(CanMessage m) {
165        if ( m.extendedOrRtr() ) {
166            return;
167        }
168        int opc = m.getElement(0);
169        int handle;
170        switch (opc) {
171            case CbusConstants.CBUS_ESTOP:
172            case CbusConstants.CBUS_RESTP:
173                stopAll();
174                break;
175            case CbusConstants.CBUS_KLOC: // Kill loco
176                log.debug("Kill loco message");
177                // Find a throttle corresponding to the handle
178                handle = m.getElement(1);
179                for (Map.Entry<Integer, CbusThrottle> entry : softThrottles.entrySet()) {
180                    CbusThrottle throttle = entry.getValue();
181                    if (throttle.getHandle() == handle) {
182                        // stop Throttle from sending keep-alives
183                        throttle.throttleDispose();
184                        // remove from abstract list
185                        forceDisposeThrottle(throttle.getLocoAddress());
186                        // Remove the Throttle from the managed list.
187                        // throttleDispose() has just set the Throttle handle to -1,
188                        // so the map key must be used, not throttle.getHandle()
189                        softThrottles.remove(entry.getKey());
190                    }
191                }
192                _singleThrottleInUse = false;
193                break;
194            case CbusConstants.CBUS_DSPD:
195                // only if emergency stop
196                if ((m.getElement(2) & 0x7f) == 1) {
197                    // Find a throttle corresponding to the handle
198                    handle = m.getElement(1);
199                    for (Map.Entry<Integer, CbusThrottle> entry : softThrottles.entrySet()) {
200                        CbusThrottle throttle = entry.getValue();
201                        if (throttle.getHandle() == handle) {
202                            // Set the throttle session to match the DSPD packet
203                            throttle.updateSpeedSetting(m.getElement(2) & 0x7f);
204                            throttle.updateIsForward((m.getElement(2) & 0x80) == 0x80);
205                        }
206                    }
207                }
208                break;
209            default:
210                break;
211        }
212    }
213
214    /**
215     * {@inheritDoc}
216     */
217    @Override
218    public void reply(CanReply m) {
219        if ( m.extendedOrRtr() ) {
220            return;
221        }
222        int opc = m.getElement(0);
223        int handle = m.getElement(1);
224
225        switch (opc) {
226            case CbusConstants.CBUS_PLOC:
227                int rcvdIntAddr = (m.getElement(2) & 0x3f) * 256 + m.getElement(3);
228                boolean rcvdIsLong = (m.getElement(2) & 0xc0) != 0;
229                DccLocoAddress rcvdDccAddr = new DccLocoAddress(rcvdIntAddr, rcvdIsLong);
230                log.debug("Throttle manager received PLOC with session {} for address {}",m.getElement(1),rcvdIntAddr);
231                if ((_handleExpected) && rcvdDccAddr.equals(_dccAddr)) {
232                    log.debug("PLOC was expected");
233                    // We're expecting an engine report and it matches our address
234                    stopThrottleRequestTimer();
235                    handle = m.getElement(1);
236                    if (!memo.hasMultipleThrottles()) {
237                        _singleThrottleInUse = true;
238                    }
239
240                    // check if the PLOC has come from a throttle session cancel notification,
241                    // only a stolen throttle for the SAME loco address can be re-bound to this session
242                    for (Map.Entry<Integer, CbusThrottle> entry : softThrottles.entrySet()) {
243                        CbusThrottle throttle = entry.getValue();
244                        if (throttle.isStolen() && rcvdDccAddr.equals(throttle.getLocoAddress())) {
245                            log.debug("setting handle from {} to {}",throttle.getHandle(),handle);
246                            // re-key the throttle under its new session handle
247                            softThrottles.remove(entry.getKey());
248                            throttle.setHandle(handle);
249                            softThrottles.put(handle, throttle);
250                            // uses timeout to help prevent steal loops
251                           // jmri.util.ThreadingUtil.runOnLayoutDelayed( () -> {
252                                throttle.setStolen(false); // sends the reactivation PCL
253                           // },500 );
254                            throttle.throttleInit(m.getElement(4), m.getElement(5), m.getElement(6), m.getElement(7));
255                            _handleExpected = false;
256                            return;
257                        }
258                    }
259
260                    // Initialise new throttle from PLOC data to allow taking over moving trains
261                    CbusThrottle throttle = new CbusThrottle((CanSystemConnectionMemo) adapterMemo, rcvdDccAddr, handle);
262                    notifyThrottleKnown(throttle, rcvdDccAddr);
263                    throttle.throttleInit(m.getElement(4), m.getElement(5), m.getElement(6), m.getElement(7));
264                    softThrottles.put(handle, throttle);
265                    _handleExpected = false;
266                }
267                break;
268            case CbusConstants.CBUS_ERR:
269                handleErr(m);
270                break;
271            case CbusConstants.CBUS_DSPD:
272                // Find a throttle corresponding to the handle
273                for (Map.Entry<Integer, CbusThrottle> entry : softThrottles.entrySet()) {
274                    CbusThrottle throttle = entry.getValue();
275                    if (throttle.getHandle() == handle) {
276                        // Set the throttle session to match the DSPD packet received
277                        throttle.updateSpeedSetting(m.getElement(2) & 0x7f);
278                        throttle.updateIsForward((m.getElement(2) & 0x80) == 0x80);
279                        // if something external to JMRI is sharing a session
280                        // dispatch is invalid
281                        throttle.setDispatchActive(false);
282                    }
283                }
284                break;
285
286            case CbusConstants.CBUS_DFUN:
287                // Find a throttle corresponding to the handle
288                for (Map.Entry<Integer, CbusThrottle> entry : softThrottles.entrySet()) {
289                    CbusThrottle throttle = entry.getValue();
290                    if (throttle.getHandle() == handle) {
291                        // if something external to JMRI is sharing a session
292                        // dispatch is invalid
293                        throttle.setDispatchActive(false);
294                        throttle.updateFunctionGroup(m.getElement(2),m.getElement(3));
295                    }
296                }
297                break;
298
299            case CbusConstants.CBUS_DFNON:
300            case CbusConstants.CBUS_DFNOF:
301                // Find a throttle corresponding to the handle
302                for (Map.Entry<Integer, CbusThrottle> entry : softThrottles.entrySet()) {
303                    CbusThrottle throttle = entry.getValue();
304                    if (throttle.getHandle() == handle) {
305                        // dispatch is invalid if something external to JMRI is sharing a session
306                        throttle.setDispatchActive(false);
307                        throttle.updateFunction(m.getElement(2), (opc == CbusConstants.CBUS_DFNON));
308                    }
309                }
310                break;
311
312            case CbusConstants.CBUS_ESTOP:
313            case CbusConstants.CBUS_RESTP:
314                stopAll();
315                break;
316            case CbusConstants.CBUS_DKEEP:
317                for (Map.Entry<Integer, CbusThrottle> entry : softThrottles.entrySet()) {
318                    CbusThrottle throttle = entry.getValue();
319                    if (throttle.getHandle() == handle) {
320                        // if something external to JMRI is sharing a session
321                        // dispatch is invalid
322                        throttle.setDispatchActive(false);
323                    }
324                }
325                break;
326            default:
327                break;
328        }
329    }
330
331    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value="SLF4J_SIGN_ONLY_FORMAT",
332                                                        justification="I18N of log message")
333    private void handleErr(CanReply m) {
334        int handle = m.getElement(1);
335        int rcvdIntAddr = (m.getElement(1) & 0x3f) * 256 + m.getElement(2);
336        boolean rcvdIsLong = (m.getElement(1) & 0xc0) != 0;
337        // DccLocoAddress rcvdDccAddr = new DccLocoAddress(rcvdIntAddr, rcvdIsLong);
338        int errCode = m.getElement(3);
339
340        boolean responseForUs = ((_handleExpected) && new DccLocoAddress(rcvdIntAddr, rcvdIsLong).equals(_dccAddr));
341
342        switch (errCode) {
343            case CbusConstants.ERR_LOCO_STACK_FULL:
344            case CbusConstants.ERR_LOCO_ADDRESS_TAKEN:
345
346                String errStr;
347                if ( errCode == CbusConstants.ERR_LOCO_STACK_FULL ){
348                    errStr = Bundle.getMessage("ERR_LOCO_STACK_FULL") + " " + rcvdIntAddr;
349                } else {
350                    errStr = Bundle.getMessage("ERR_LOCO_ADDRESS_TAKEN", rcvdIntAddr);
351                }
352
353                // log.debug("handlexpected {} _dccAddr {} got {} ", _handleExpected, _dccAddr, rcvdDccAddr);
354
355                if (responseForUs) { // We were expecting an engine report and it matches our address
356                    log.debug("Failed throttle request due to ERR");
357                    _handleExpected = false;
358                    stopThrottleRequestTimer();
359
360                    // if this is the result of a share or steal request,
361                    // we need to stop here and inform the ThrottleListener
362                    if ( _handleExpectedSecondLevelRequest ){
363                        failedThrottleRequest(_dccAddr, errStr);
364                        return;
365                    }
366
367                    // so this is the message from the 1st normal request
368                    // now we check the command station,
369                    // and notify the ThrottleListener ()
370
371                    boolean steal = false;
372                    boolean share = false;
373
374                    CbusCommandStation cs = (CbusCommandStation) memo.get(CommandStation.class);
375
376                    if ( cs != null ) {
377                        log.debug("cs says can steal {}, can share {}", cs.isStealAvailable(), cs.isShareAvailable() );
378                        steal = cs.isStealAvailable();
379                        share = cs.isShareAvailable();
380                    }
381
382                    if ( !steal && !share ){
383                        failedThrottleRequest(_dccAddr, errStr);
384                    }
385                    else if ( steal && share ){
386                        notifyDecisionRequest(_dccAddr, DecisionType.STEAL_OR_SHARE);
387                    }
388                    else if ( steal ){
389                        notifyDecisionRequest(_dccAddr, DecisionType.STEAL);
390                    }
391                    else { // must be share
392                        notifyDecisionRequest(_dccAddr, DecisionType.SHARE);
393                    }
394                } else {
395                    log.debug("ERR address not matched");
396                }
397                break;
398
399            case CbusConstants.ERR_SESSION_NOT_PRESENT:
400                // most likely called via a command station being reset or
401                // coming back online
402                log.warn("{}",Bundle.getMessage("ERR_SESSION_NOT_PRESENT",handle));
403
404                if (responseForUs) {
405                    // We were expecting an engine report and it matches our address
406                    _handleExpected = false;
407                    failedThrottleRequest(_dccAddr, Bundle.getMessage("CBUS_ERROR")
408                        + Bundle.getMessage("ERR_SESSION_NOT_PRESENT",handle));
409                    log.warn("Session not present when expecting a session number");
410                }
411
412                // check if it's a JMRI throttle session,
413                // Inform the throttle associated with this session handle, if any
414                for (Map.Entry<Integer, CbusThrottle> entry : softThrottles.entrySet()) {
415                    CbusThrottle throttle = entry.getValue();
416                    if (throttle.getHandle() == handle) {
417                        log.warn("Cancelling JMRI Throttle Session {} for loco {}",
418                            handle,
419                            throttle.getLocoAddress().toString()
420                        );
421                        attemptRecoverThrottle(throttle);
422                        break;
423                    }
424                }
425                break;
426            case CbusConstants.ERR_CONSIST_EMPTY:
427                log.warn("{} {}",Bundle.getMessage("ERR_CONSIST_EMPTY"), handle);
428                break;
429            case CbusConstants.ERR_LOCO_NOT_FOUND:
430                log.warn("{} {}", Bundle.getMessage("ERR_LOCO_NOT_FOUND"), handle);
431                break;
432            case CbusConstants.ERR_CAN_BUS_ERROR:
433                log.error("{}",Bundle.getMessage("ERR_CAN_BUS_ERROR"));
434                if (!GraphicsEnvironment.isHeadless() && !canErrorDialogVisible ) {
435                    canErrorDialogVisible = true;
436                    ThreadingUtil.runOnGUI(() ->
437                        JmriJOptionPane.showMessageDialogNonModal(null, // parent
438                            Bundle.getMessage("ERR_CAN_BUS_ERROR"), // message
439                            Bundle.getMessage("CBUS_ERROR"), // title
440                            JmriJOptionPane.ERROR_MESSAGE, // message type
441                            () -> canErrorDialogVisible = false )); // callback
442                }
443                return;
444            case CbusConstants.ERR_INVALID_REQUEST:
445                log.error("{}", Bundle.getMessage("ERR_INVALID_REQUEST"));
446                if (!GraphicsEnvironment.isHeadless() && !invalidErrorDialogVisible){
447                    invalidErrorDialogVisible = true;
448                    ThreadingUtil.runOnGUI(() ->
449                        JmriJOptionPane.showMessageDialogNonModal(null, // parent
450                            Bundle.getMessage("ERR_INVALID_REQUEST"), // message
451                            Bundle.getMessage("CBUS_ERROR"), // title
452                            JmriJOptionPane.ERROR_MESSAGE, // message type
453                            () -> invalidErrorDialogVisible = false )); // callback
454                }
455                return;
456            case CbusConstants.ERR_SESSION_CANCELLED:
457                // There will be a session cancelled error for the other throttle(s)
458                // when you are stealing, but as you don't yet have a session id, it
459                // won't match so you will ignore it, then a PLOC will come with that
460                // session id and your requested loco number which is giving it to you.
461
462                log.debug("{}", Bundle.getMessage("ERR_SESSION_CANCELLED",handle));
463
464                // Inform the throttle associated with this session handle, if any
465                for (Map.Entry<Integer, CbusThrottle> entry : softThrottles.entrySet()) {
466                    CbusThrottle throttle = entry.getValue();
467                    if (throttle.getHandle() == handle) {
468                        if (throttle.isStolen()){ // already actioned
469                            log.debug("external steal already actioned, returning");
470                            return;
471                        }
472                        log.warn("External Steal / Cancel for loco {} Session {} ",throttle.getLocoAddress(), handle );
473                        attemptRecoverThrottle(throttle);
474                        break;
475                    }
476                }
477                break;
478            default:
479                log.error("{} error code: {}", Bundle.getMessage("ERR_UNKNOWN"), errCode);
480                break;
481        }
482    }
483
484    /**
485     * Attempts Throttle Recovery when a session has been lost
486     */
487    private void attemptRecoverThrottle(CbusThrottle throttle){
488
489        log.debug("start of recovery, current throttle stolen {} session {} num recovr attempts {} hashmap size {}",
490            throttle.isStolen(), throttle.getHandle(), throttle.getNumRecoverAttempts(),
491            softThrottles.size() );
492
493        throttle.increaseNumRecoverAttempts();
494
495        if (throttle.getNumRecoverAttempts() > 10) { // catch runaways
496            _handleExpected = false;
497            throttle.throttleDispose(); // stop throttle keep-alive messages, send PCL ThrottleConnected false
498            showSessionCancelDialogue(throttle.getLocoAddress());
499            removeSoftThrottle(throttle); // remove from local list
500            forceDisposeThrottle( throttle.getLocoAddress() ); // remove from JMRI share list
501            return; // throttle disposed, do not request a further steal / share
502        }
503
504        throttle.setStolen(true);
505        throttle.setHandle(-1);
506
507        boolean steal = false;
508        boolean share = false;
509
510        CbusCommandStation cs = (CbusCommandStation) memo.get(CommandStation.class);
511        if ( cs != null ) {
512            log.debug("cs says can steal {}, can share {}", cs.isStealAvailable(), cs.isShareAvailable() );
513            steal = cs.isStealAvailable();
514            share = cs.isShareAvailable();
515        }
516
517        if (share && InstanceManager.getDefault(ThrottlesPreferences.class).isSilentShare()){
518            // share is available on command station AND silent share preference checked
519            log.info("Requesting Silent Share loco {} to regain a session",throttle.getLocoAddress());
520            ThreadingUtil.runOnLayoutDelayed( () -> {
521                startThrottleRequestTimer(true);
522                requestThrottleSetup(throttle.getLocoAddress(), DecisionType.SHARE);
523            },1000);
524        }
525        else if (steal && InstanceManager.getDefault(ThrottlesPreferences.class).isSilentSteal()) {
526            // steal is available on command station AND silent steal preference checked
527            log.info("Requesting Silent Steal loco {} to regain a session",throttle.getLocoAddress());
528            ThreadingUtil.runOnLayoutDelayed( () -> {
529                startThrottleRequestTimer(true);
530                requestThrottleSetup(throttle.getLocoAddress(), DecisionType.STEAL);
531            },1000);
532        } else {
533            throttle.throttleDispose(); // stop throttle keep-alive messages, send PCL ThrottleConnected false
534            showSessionCancelDialogue(throttle.getLocoAddress());
535            removeSoftThrottle(throttle); // remove from local list
536            forceDisposeThrottle( throttle.getLocoAddress() ); // remove from JMRI share list
537        }
538    }
539
540    /**
541     * Remove a Throttle from the list of managed Throttles.
542     * <p>
543     * Removes by Throttle instance rather than by session handle, as the
544     * handle is set to -1 when a Throttle is stolen or disposed.
545     * @param throttle the Throttle to remove
546     */
547    private void removeSoftThrottle(CbusThrottle throttle) {
548        boolean removed = softThrottles.entrySet().removeIf( entry -> entry.getValue() == throttle );
549        log.debug("throttle {} removed from managed list: {}", throttle, removed);
550    }
551
552    /**
553     * Get the number of Throttles in the managed session list.
554     * Package access for testing.
555     * @return number of managed Throttles, including any awaiting session recovery.
556     */
557    int getManagedThrottleCount() {
558        return softThrottles.size();
559    }
560
561    /**
562     * CBUS has a dynamic Dispatch function, defaulting to false
563     * {@inheritDoc}
564     */
565    @Override
566    public boolean hasDispatchFunction() {
567        return false;
568    }
569
570    /**
571     * Any address is potentially a long address.
572     * {@inheritDoc}
573     */
574    @Override
575    public boolean canBeLongAddress(int address) {
576        return address > 0;
577    }
578
579    /**
580     * Address 127 and below is a short address.
581     * {@inheritDoc}
582     */
583    @Override
584    public boolean canBeShortAddress(int address) {
585        return address < 128;
586    }
587
588    /**
589     * Short and long address spaces overlap and are not unique.
590     * @return always false.
591     * {@inheritDoc}
592     */
593    @Override
594    public boolean addressTypeUnique() {
595        return false;
596    }
597
598    /**
599     * Local method for deciding short/long address.
600     * @param num the address number
601     * @return true if address equal to or greater than 128.
602     */
603    static boolean isLongAddress(int num) {
604        return (num >= 128);
605    }
606
607    /**
608     * Hardware has a stealing implementation.
609     * {@inheritDoc}
610     */
611    @Override
612    public boolean enablePrefSilentStealOption() {
613        return true;
614    }
615
616    /**
617     * Hardware has a sharing implementation.
618     * {@inheritDoc}
619     */
620    @Override
621    public boolean enablePrefSilentShareOption() {
622        return true;
623    }
624
625    /**
626     * CBUS Hardware will make its own decision on preferred option.
627     * <p>
628     * This is the default for scripts that do NOT have a GUI for asking what to do when
629     * a steal / share decision is required.
630     * {@inheritDoc}
631     */
632    @Override
633    protected void makeHardwareDecision(LocoAddress address,DecisionType question){
634        // no need to check if share / steal currently enabled on command station,
635        // this has already been done to produce the correct question
636        switch (question) {
637            case STEAL:
638                // share has been disabled in command station
639                responseThrottleDecision(address, null, DecisionType.STEAL );
640                break;
641            case SHARE:
642                // steal has been disabled in command station
643                responseThrottleDecision(address, null, DecisionType.SHARE );
644                break;
645            default: // case STEAL_OR_SHARE:
646                if ( InstanceManager.getDefault(ThrottlesPreferences.class).isSilentSteal() ){
647                    responseThrottleDecision(address, null, DecisionType.STEAL );
648                }
649                else {
650                    responseThrottleDecision(address, null, DecisionType.SHARE );
651                }
652                break;
653        }
654    }
655
656    /**
657     * Send a request to steal or share a requested throttle.
658     * <p>
659     * {@inheritDoc}
660     *
661     */
662    @Override
663    public void responseThrottleDecision(LocoAddress address, ThrottleListener l, DecisionType decision) {
664        log.debug("Received {} response for Loco {}, listener {}",decision,address,l);
665        startThrottleRequestTimer(false);
666        requestThrottleSetup(address,decision);
667    }
668
669    private TimerTask throttleRequestTimer;
670
671    /**
672     * Start timer to wait for command station to respond to RLOC or GLOC
673     */
674    private void startThrottleRequestTimer(boolean isRecovery) {
675        stopThrottleRequestTimer(); // do not leave a previous request timer running
676        throttleRequestTimer = new TimerTask() {
677            @Override
678            public void run() {
679                timeout(isRecovery);
680            }
681        };
682        TimerUtil.schedule(throttleRequestTimer, ( THROTTLE_TIMEOUT ) );
683    }
684
685    private void stopThrottleRequestTimer(){
686        if (throttleRequestTimer!=null){
687            throttleRequestTimer.cancel();
688        }
689        throttleRequestTimer = null;
690    }
691
692    /**
693     * Internal routine to notify failed throttle request a timeout
694     */
695    private void timeout(boolean isRecovery) {
696        log.debug("Throttle request (RLOC or PLOC) timed out");
697        stopThrottleRequestTimer();
698        if (isRecovery){
699            log.warn("Session recovery not possible for {}",_dccAddr);
700            forceDisposeThrottle( _dccAddr ); // remove from JMRI share list
701
702            for (Map.Entry<Integer, CbusThrottle> entry : softThrottles.entrySet()) {
703                CbusThrottle throttle = entry.getValue();
704                if (Objects.equals(throttle.getLocoAddress(), _dccAddr)) {
705                    throttle.throttleDispose();
706                    showSessionCancelDialogue(_dccAddr);
707                    // handle is now -1, remove by map key
708                    softThrottles.remove(entry.getKey());
709                }
710            }
711        }
712        else { // not in recovery, normal request timeout, is command station connected?
713            failedThrottleRequest(_dccAddr, Bundle.getMessage("ERR_THROTTLE_TIMEOUT"));
714        }
715    }
716
717    /**
718     * MERG CBUS Throttle sessions default to 128 SS.
719     * This can be changed by a subsequent message from Throttle to CS,
720     * or by message from Command Station to CbusThrottle.
721     * Supported modes are 128, 28 and 14.
722     * {@inheritDoc }
723     */
724    @Override
725    public EnumSet<SpeedStepMode> supportedSpeedModes() {
726        return EnumSet.of(SpeedStepMode.NMRA_DCC_128
727                , SpeedStepMode.NMRA_DCC_28
728                , SpeedStepMode.NMRA_DCC_14);
729    }
730
731    /**
732     * {@inheritDoc}
733     */
734    @Override
735    public boolean disposeThrottle(DccThrottle t, ThrottleListener l) {
736        log.debug("disposeThrottle called for {}", t);
737        if (t instanceof CbusThrottle) {
738            log.debug("Cbus Dispose calling abstract Throttle manager dispose");
739            if (super.disposeThrottle(t, l)) {
740
741                CbusThrottle lnt = (CbusThrottle) t;
742                lnt.releaseFromCommandStation();
743                lnt.throttleDispose();
744                // forceDisposeThrottle( (DccLocoAddress) lnt.getLocoAddress() );
745                log.debug("called throttleDispose");
746                // The KLOC sent by releaseFromCommandStation is echoed back to message()
747                // after the handle has been set to -1, so it will not match there;
748                // remove the Throttle from the managed list here.
749                removeSoftThrottle(lnt);
750                _singleThrottleInUse = false;
751                return true;
752            }
753        }
754        return false;
755    }
756
757    /**
758     * {@inheritDoc}
759     */
760    @Override
761    protected void forceDisposeThrottle(LocoAddress la) {
762        super.forceDisposeThrottle(la);
763        _singleThrottleInUse = false;
764    }
765
766    /**
767     * {@inheritDoc}
768     */
769    @Override
770    protected void updateNumUsers( LocoAddress la, int numUsers ){
771        log.debug("throttle {} notification that num. users is now {}",la,numUsers);
772        for (Map.Entry<Integer, CbusThrottle> entry : softThrottles.entrySet()) {
773            CbusThrottle throttle = entry.getValue();
774            if (Objects.equals(throttle.getLocoAddress(), la)) {
775                if ((numUsers == 1) && throttle.getSpeedSetting() > 0) {
776                    throttle.setDispatchActive(true);
777                    return;
778                }
779                throttle.setDispatchActive(false);
780            }
781        }
782    }
783
784    /**
785     * {@inheritDoc}
786     */
787    @Override
788    public void cancelThrottleRequest(LocoAddress address, ThrottleListener l) {
789
790        // calling super removes the ThrottleListener from the callback list,
791        // The listener which has just sent the cancel doesn't need notification
792        // of the cancel but other listeners might
793        super.cancelThrottleRequest(address, l);
794        failedThrottleRequest(address, "Throttle Request " + address + " Cancelled.");
795    }
796
797    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(CbusThrottleManager.class);
798
799}