001package jmri.jmrix.loconet;
002
003import java.util.EnumSet;
004import java.util.Hashtable;
005import java.util.concurrent.LinkedBlockingQueue;
006import jmri.DccLocoAddress;
007import jmri.DccThrottle;
008import jmri.LocoAddress;
009import jmri.SpeedStepMode;
010import jmri.ThrottleListener;
011import jmri.jmrix.AbstractThrottleManager;
012import org.slf4j.Logger;
013import org.slf4j.LoggerFactory;
014
015/**
016 * LocoNet implementation of a ThrottleManager.
017 * <p>
018 * Works in cooperation with the SlotManager, which actually handles the
019 * communications.
020 *
021 * @see SlotManager
022 * @author Bob Jacobsen Copyright (C) 2001
023 * @author B. Milhaupt, Copyright (C) 2018
024 */
025public class LnThrottleManager extends AbstractThrottleManager implements SlotListener {
026
027    protected SlotManager slotManager;
028    protected LnTrafficController tc;
029
030    /**
031     * Constructor. Gets a reference to the LocoNet SlotManager.
032     *
033     * @param memo connection's memo
034     */
035    public LnThrottleManager(LocoNetSystemConnectionMemo memo) {
036        super(memo);
037        this.slotManager = memo.getSlotManager();
038        this.tc = memo.getLnTrafficController();
039        requestList = new LinkedBlockingQueue<>();
040        slotForAddress = new Hashtable<>();
041    }
042
043    /**
044     * LocoNet allows multiple throttles for the same device.
045     * <p>
046     * {@inheritDoc}
047     * @return false always
048     */
049    @Override
050    protected boolean singleUse() {
051        return false;
052    }
053    
054    /**
055     * Display the Silent Stealing checkbox option in Throttles Preferences
056     */
057    @Override
058    public boolean enablePrefSilentStealOption() {
059        return true;
060    }
061
062    /**
063     * Start creating a Throttle object.
064     *
065     * This returns directly, having arranged for the Throttle object to be
066     * delivered via callback since there are situations where the command
067     * station does not respond, (slots full, command station powered off,
068     * others?) this code will retry and then fail the request if no response
069     * occurs.
070     *
071     * @param address locomotive address to be controlled
072     * @param control true if throttle wishes to control the speed and direction
073     * of the loco.
074     */
075    @Override
076    public void requestThrottleSetup(LocoAddress address, boolean control) {
077        log.debug("requestThrottleSetup: address {}, control {}", address, control);
078        // Checking requestOutstanding and setting it true must be atomic --
079        // see its declaration below for the race this closes.
080        boolean handleNow;
081        synchronized (this) {
082            if (requestOutstanding) {
083                handleNow = false;
084            } else {
085                requestOutstanding = true;
086                handleNow = true;
087            }
088        }
089        if (handleNow) {
090           // handle this now
091           processThrottleSetupRequest(address, control);
092        } else {
093           try {
094              // queue this request for later.
095              requestList.put(new ThrottleRequest(address,control));
096           } catch (InterruptedException ie) {
097              log.error("Interrupted while trying to store throttle request");
098              synchronized (this) {
099                  requestOutstanding = false;
100              }
101           }
102        }
103     }
104
105    /**
106     * Processes the next loco from the queue of requested locos for which to get
107     * a LocoNetThrottle.
108     */
109    protected void processQueuedThrottleSetupRequest() {
110        // Same atomicity requirement as requestThrottleSetup() above.
111        boolean handleNow;
112        synchronized (this) {
113            if (!requestOutstanding && (requestList.size() != 0 )) {
114                requestOutstanding = true;
115                handleNow = true;
116            } else {
117                handleNow = false;
118            }
119        }
120        if (handleNow) {
121           try {
122              ThrottleRequest tr = requestList.take();
123              processThrottleSetupRequest(tr.getAddress(), tr.getControl());
124           } catch (InterruptedException ie) {
125              log.error("Interrupted while trying to process process throttle request");
126              synchronized (this) {
127                  requestOutstanding = false;
128              }
129           }
130        }
131     }
132
133    /**
134     * Begin the processing of a Throttle Request.
135     *
136     * @param address Loco address
137     * @param control whether the throttle object wants to control the loco
138     */
139    private void processThrottleSetupRequest(LocoAddress address, boolean control) {
140        pendingRequestAddress = new DccLocoAddress(address.getNumber(), isLongAddress(address.getNumber()));
141        slotManager.slotFromLocoAddress(address.getNumber(), this);  //first try
142
143        class RetrySetup implements Runnable { // setup for retries and failure check
144
145            final DccLocoAddress address;
146            final SlotListener list;
147
148            RetrySetup(DccLocoAddress address, SlotListener list) {
149                this.address = address;
150                this.list = list;
151            }
152
153            @Override
154            public void run() {
155                int attempts = 1; // already tried once above
156                // Was 10 (10s total) -- too short for a command station
157                // still working through a backlog of rapid sequential slot
158                // requests (e.g. building/tearing down a large consist).
159                // This is a dedicated background thread, so waiting longer
160                // here is safe.
161                int maxAttempts = 20;
162                while (attempts <= maxAttempts) {
163                    try {
164                        Thread.sleep(1000); // wait one second
165                    } catch (InterruptedException ex) {
166                        return; // stop waiting if slot is found or error occurs
167                    }
168                    String again = "";
169                    if (attempts < maxAttempts) {
170                        slotManager.slotFromLocoAddress(address.getNumber(), list);
171                        again = ", trying again."; // NOI18N
172                    }
173                    log.debug("No response to slot request for {}, attempt {} {}", address, attempts, again);
174                    attempts++;
175                }
176                log.error("No response to slot request for {} after {} attempts.", address, attempts - 1); // NOI18N
177                // A listener throwing out of notifyFailedThrottleRequest()
178                // must not skip the cleanup below -- otherwise
179                // requestOutstanding stays stuck true forever and wedges
180                // every future throttle request for the rest of the session.
181                try {
182                    failedThrottleRequest(address, "Failed to get response from command station");
183                } catch (RuntimeException ex) {
184                    log.error("Listener threw while handling failed throttle request for {} -- continuing anyway", address, ex);
185                } finally {
186                    requestOutstanding = false;
187                    pendingRequestAddress = null;
188                    processQueuedThrottleSetupRequest();
189                }
190            }
191        }
192
193        retrySetupThread = new Thread(
194                new RetrySetup(new DccLocoAddress(address.getNumber(),
195                        isLongAddress(address.getNumber())), this));
196        retrySetupThread.setName("LnThrottleManager RetrySetup " + address);
197        retrySetupThread.start();
198        synchronized (this) {
199            waitingForNotification.put(address.getNumber(), retrySetupThread);
200        }
201    }
202
203    volatile Thread retrySetupThread;
204
205    // Address a pending slot request was actually made for. This manager
206    // is a single shared SlotListener for whatever address it's currently
207    // requesting, so a stale/delayed response from an earlier, already-
208    // completed request can otherwise get misattributed to the current
209    // one. Checked in notifyChangedSlot() before acting on a response; a
210    // mismatch is simply ignored since RetrySetup already re-issues the
211    // request every second regardless.
212    private volatile DccLocoAddress pendingRequestAddress = null;
213
214    Hashtable<Integer, Thread> waitingForNotification = new Hashtable<>(5);
215
216    Hashtable<Integer, LocoNetSlot> slotForAddress;
217    LinkedBlockingQueue<ThrottleRequest> requestList;
218    // volatile, and only ever check-and-set together with
219    // pendingRequestAddress inside synchronized(this) -- see
220    // requestThrottleSetup()/processQueuedThrottleSetupRequest(). Without
221    // that, two threads (e.g. the EDT building a consist and a separate
222    // JSON throttle request) can both observe this false at once and both
223    // proceed, each overwriting pendingRequestAddress and getting checked
224    // against the other's slot response.
225    volatile boolean requestOutstanding = false;
226
227    /**
228     * LocoNet does have a Dispatch function.
229     *
230     * @return true
231     */
232    @Override
233    public boolean hasDispatchFunction() {
234        return true;
235    }
236
237    /**
238     * What speed modes are supported by this system? value should be xor of
239     * possible modes specified by the DccThrottle interface.
240     *
241     * @return an integer containing the combined speed step modes supported
242     */
243    @Override
244    public EnumSet<SpeedStepMode> supportedSpeedModes() {
245        return EnumSet.of(SpeedStepMode.NMRA_DCC_128
246                , SpeedStepMode.NMRA_DCC_28
247                , SpeedStepMode.MOTOROLA_28
248                , SpeedStepMode.NMRA_DCC_14);
249    }
250
251    /**
252     * Get notification that an address has changed slot. This method creates a
253     * throttle for all ThrottleListeners of that address and notifies them via
254     * the ThrottleListener.notifyThrottleFound method.
255     *
256     * @param s LocoNet slot which has been changed
257     */
258    @Override
259    public void notifyChangedSlot(LocoNetSlot s) {
260        log.debug("notifyChangedSlot - slot {}, slotStatus {}", s.getSlot(), Integer.toHexString(s.slotStatus()));
261        // This is invoked only if the SlotManager knows that the LnThrottleManager is
262        // interested in the address associated with this slot.
263
264        // Reject a response that doesn't match what's actually being
265        // waited for (see pendingRequestAddress above) -- RetrySetup's
266        // 1-second retry loop recovers naturally rather than this
267        // silently completing the wrong address's acquisition.
268        DccLocoAddress expected = pendingRequestAddress;
269        if (expected != null && s.locoAddr() != expected.getNumber()) {
270            log.warn("notifyChangedSlot(): requested slot for {} but got slot {} for address {} instead -- ignoring stale/mismatched response",
271                    expected, s.getSlot(), s.locoAddr());
272            return;
273        }
274
275        // need to check to see if the slot is in a suitable state for creating a throttle.
276        if (s.slotStatus() == LnConstants.LOCO_IN_USE) {
277            // loco is already in-use
278            log.warn("slot {} address {} is already in-use.",
279                    s.getSlot(), s.locoAddr());
280            // is the throttle ID the same as for this JMRI instance?  If not, do not accept the slot.
281            if ((s.id() != 0) && s.id() != throttleID) {
282                // notify the LnThrottleManager about failure of acquisition.
283                // NEED TO TRIGGER THE NEW "STEAL REQUIRED" FUNCTIONALITY HERE
284                //note: throttle listener expects to have "callback" method notifyDecisionRequired
285                //invoked if a "steal" is required.  Make that happen as part of the "acquisition" process
286                synchronized (this) {
287                    slotForAddress.put(s.locoAddr(), s);
288                }
289                notifyStealRequest(s.locoAddr());
290                return;
291            }
292            // shared throttle / already ours
293            notifyComplete(commitToAcquireThrottle(s),s);
294            return;
295        }
296        commitToAcquireThrottle(s);
297    }
298
299    /**
300     * Making progress in the process of acquiring a throttle.
301     *
302     * @param s slot to be acquired
303     */
304    private DccThrottle commitToAcquireThrottle(LocoNetSlot s) {
305        // haven't identified a particular reason to refuse throttle acquisition at this time...
306        return createThrottle((LocoNetSystemConnectionMemo) adapterMemo, s);
307        // the rest is done when the write of the throttle ID has been acknowledged  in the throttle
308        // by calling notifyComplete
309     }
310
311    /**
312     * Called from the throttle slot when the final write of throttle id has been
313     * completed, and the slot is set as initialized, or called directly for our own shared throttles.
314     * @param t the throttle
315     * @param s the lot.
316     */
317     protected void notifyComplete(DccThrottle t, LocoNetSlot s) {
318         // end the waiting thread since we got a response
319         s.notifySlotListeners(); // make sure other listeners for this slot
320                                  // know about what's going on!
321         notifyThrottleKnown(t, new DccLocoAddress(s.locoAddr(), isLongAddress(s.locoAddr())));
322         synchronized (this) {
323             if (waitingForNotification.containsKey(s.locoAddr())) {
324                 log.debug(
325                         "LnThrottleManager.notifyChangedSlot() - removing throttle acquisition notification flagging for address {}",
326                         s.locoAddr());
327                 waitingForNotification.get(s.locoAddr()).interrupt();
328                 waitingForNotification.remove(s.locoAddr());
329             } else {
330                 log.debug(
331                         "LnThrottleManager.notifyChangedSlot() - ignoring slot notification for slot {}, address {} account not attempting to acquire that address",
332                         s.getSlot(), s.locoAddr());
333             }
334             slotForAddress.remove(s.locoAddr());
335         }
336         requestOutstanding = false;
337         pendingRequestAddress = null;
338         processQueuedThrottleSetupRequest();
339     }
340
341    /**
342     * Loco acquisition failed. Propagate the failure message to the (GUI)
343     * throttle.
344     *
345     * @param address of the loco which could not be acquired
346     * @param cause reason for the failure
347     */
348    public void notifyRefused(int address, String cause) {
349        //end the waiting thread since we got a failure response
350        synchronized (this) {
351            if (waitingForNotification.containsKey(address)) {
352                waitingForNotification.get(address).interrupt();
353                waitingForNotification.remove(address);
354                // notify the throttle - in some other thread!
355
356                class InformRejection implements Runnable {
357                    // inform the throttle from a new thread, so that
358                    // the modal dialog box doesn't block other LocoNet
359                    // message handling
360
361                    final int address;
362                    final String cause;
363
364                    InformRejection(int address, String s) {
365                        this.address = address;
366                        this.cause = s;
367                    }
368
369                    @Override
370                    public void run() {
371
372                        log.debug("New thread launched to inform throttle user of failure to acquire loco {} - {}", address, cause);
373                        failedThrottleRequest(new DccLocoAddress(address, isLongAddress(address)), cause);
374                    }
375
376                }
377                Thread thr = new Thread(new InformRejection(address, cause));
378                thr.start();
379            }
380            slotForAddress.remove(address);
381        }
382        requestOutstanding = false;
383        pendingRequestAddress = null;
384        processQueuedThrottleSetupRequest();
385    }
386
387
388    /**
389     * Create a LocoNet Throttle to control a loco.
390     * <p>
391     * This is called during the loco acquisition process by logic within
392     * LnThrottleManager.  Generally, it should not be directly called by other
393     * methods.
394     *
395     * @param memo connection memo used by the throttle for communications
396     * @param s slot holding an acquired loco
397     * @return throttle holding an acquired loco
398     */
399    DccThrottle createThrottle(LocoNetSystemConnectionMemo memo, LocoNetSlot s) {
400        log.debug("createThrottle: slot {}", s.getSlot());
401        return new LocoNetThrottle(memo, s);
402    }
403
404    /**
405     * Determines if the loco address is a long address.
406     * <p>
407     * For LocoNet, address 128 and above is a long address.
408     *
409     * @param address to be checked
410     * @return true if long address, else false
411     */
412    @Override
413    public boolean canBeLongAddress(int address) {
414        return isLongAddress(address);
415    }
416
417    /**
418     * Determines if the loco address is a short address.
419     * <p>
420     * For LocoNet, address 127 and below is a short address
421     *
422     * @param address to be checked
423     * @return true if short address, else false
424     */
425    @Override
426    public boolean canBeShortAddress(int address) {
427        return !isLongAddress(address);
428    }
429
430    /**
431     * Reports whether all loco addresses are uniquely long or short, without any
432     * ambiguity for any address.
433     * <p>
434     * For LocoNet, there are no ambiguous addresses.
435     *
436     * @return true
437     */
438    @Override
439    public boolean addressTypeUnique() {
440        return true;
441    }
442
443    /**
444     * Local method for deciding short/long address.
445     *
446     * @param num address to be checked
447     * @return true if num is a long address else false
448     */
449    protected static boolean isLongAddress(int num) {
450        return (num >= 128);
451    }
452
453    /**
454     * Disposes a LnThrottle object.
455     * <p>
456     * Generally, this will cause the slot to be made "common" and the LnThrottle
457     * is disposed of.
458     * <p>
459     * After disposal, the throttle may not be used to control the loco.
460     *
461     * @param t is a throttle to be disposed of
462     * @param l is the listener for the throttle
463     * @return false if throttle is not a LocoNetThrottle, else true
464     */
465    @Override
466    public boolean disposeThrottle(DccThrottle t, ThrottleListener l) {
467        log.debug("disposeThrottle - throttle {}", t.getLocoAddress());
468        if (t instanceof LocoNetThrottle) {
469            if (super.disposeThrottle(t, l)) {
470                LocoNetThrottle lnt = (LocoNetThrottle) t;
471                lnt.throttleDispose();
472                return true;
473            }
474        }
475        return false;
476    }
477
478    /**
479     * Dispatches a loco from a LnThrottle object.
480     * <p>
481     * Generally, this will cause the slot to be made "common" and then linked via
482     * the "Dispatch" slot.
483     * <p>
484     * After dispatching, the throttle may not be used to control the loco.
485     * You should check getUsageCountBefore calling as it will fail if not 1.
486     *
487     * @param t is a throttle to be disposed of
488     * @param l is the listener for the throttle
489     */
490    @Override
491    public void dispatchThrottle(DccThrottle t, ThrottleListener l) {
492        log.debug("dispatchThrottle - throttle {}", t.getLocoAddress());
493        // Use slot to dispatch, then release
494        if (t instanceof LocoNetThrottle) {
495            // only dispatch if its the last throttle use
496            if (super.getThrottleUsageCount(t.getLocoAddress()) == 1)  {
497                ((LocoNetThrottle) t).dispatchThrottle(t, l);
498            } else {
499                return;
500            }
501        }
502        super.releaseThrottle(t, l);
503    }
504
505    /**
506     * Dispatch a loco from a LnThrottle object.
507     * <p>
508     * Generally, this will cause the slot to be made "common".
509     * <p>
510     * After disposal, the throttle may not be used to control the loco.
511     *
512     * @param t is a throttle to be disposed of
513     * @param l is the listener for the throttle
514     */
515    @Override
516    public void releaseThrottle(DccThrottle t, ThrottleListener l) {
517        log.debug("releaseThrottle - throttle {}", t.getLocoAddress());
518        super.releaseThrottle(t, l);
519    }
520
521    /**
522     * Cancels the loco acquisition process when throttle acquisition of a loco
523     * fails.
524     *
525     * @param address loco address which could not be acquired
526     * @param reason for the failure
527     */
528    @Override
529    public void failedThrottleRequest(LocoAddress address, String reason) {
530        super.failedThrottleRequest(address, reason);
531        log.debug("failedThrottleRequest - address {}, reason {}", address, reason);
532        //now end and remove any waiting thread
533        synchronized (this) {
534            if (waitingForNotification.containsKey(address.getNumber())) {
535                waitingForNotification.get(address.getNumber()).interrupt();
536                waitingForNotification.remove(address.getNumber());
537            }
538            slotForAddress.remove(address.getNumber());
539        }
540        requestOutstanding = false;
541        pendingRequestAddress = null;
542        processQueuedThrottleSetupRequest();
543    }
544
545    /**
546     * Cancel a request for a throttle.
547     *
548     * @param address The decoder address desired.
549     *                address.
550     * @param l       The ThrottleListener cancelling request for a throttle.
551     */
552    @Override
553    public void cancelThrottleRequest(LocoAddress address, ThrottleListener l) {
554        
555        // calling super removes the ThrottleListener from the callback list,
556        // The listener which has just sent the cancel doesn't need notification
557        // of the cancel but other listeners might
558        super.cancelThrottleRequest(address, l);
559        
560        failedThrottleRequest(address, "Throttle Request " + address + " Cancelled.");
561        
562        int loconumber = address.getNumber();
563        log.debug("cancelThrottleRequest - loconumber {}", loconumber);
564        synchronized (this) {
565            if (waitingForNotification.containsKey(loconumber)) {
566                waitingForNotification.get(loconumber).interrupt();
567                waitingForNotification.remove(loconumber);
568            }
569            slotForAddress.remove(loconumber);
570        }
571        requestOutstanding = false;
572        pendingRequestAddress = null;
573        processQueuedThrottleSetupRequest();
574    }
575
576    protected int throttleID = 0x0171;
577
578    /**
579     * Get the ThrottleID value for this throttle.
580     *
581     * @return the ThrottleID value
582     */
583    public int getThrottleID() {
584        return throttleID;
585    }
586
587    /**
588     * {@inheritDoc}
589     * Dispose of this manager, typically for testing.
590     */
591    @Override
592    public void dispose() {
593        if (retrySetupThread != null) {
594            try {
595                retrySetupThread.interrupt();
596                retrySetupThread.join();
597            } catch (InterruptedException ex) {
598                log.warn("dispose interrupted");
599            }
600        }
601    }
602
603    /**
604     * Inform the requesting throttle object (not the connection-specific throttle
605     * implementation!)  that the address is in-use and the throttle user may
606     * either choose to "steal" the address, or quit the acquisition process.
607     * The LocoNet acquisition process "retry" timer is stopped as part of this
608     * process, since a positive response has been received from the command station
609     * and since user intervention is required.
610     *
611     * Reminder: for LocoNet throttles which are not using "expanded slot"
612     * functionality, "steal" really means "share".  For those LocoNet throttles
613     * which are using "expanded slots", "steal" really means take control and
614     * let the command station issue a "StealZap" LocoNet message to the other throttle.
615     *
616     * @param locoAddr address of DCC loco or consist
617     */
618    public void notifyStealRequest(int locoAddr) {
619        // need to find the "throttleListener" associated with the request for locoAddr, and
620        // send that "throttleListener" a notification that the command station needs
621        // permission to "steal" the loco address.
622        synchronized (this) {
623            if (waitingForNotification.containsKey(locoAddr)) {
624                waitingForNotification.get(locoAddr).interrupt();
625                waitingForNotification.remove(locoAddr);
626
627                notifyDecisionRequest(new DccLocoAddress(locoAddr, isLongAddress(locoAddr)), ThrottleListener.DecisionType.STEAL);
628            }
629        }
630    }
631
632    /**
633     * Perform the actual "Steal" of the requested throttle.
634     * <p>
635     * This is a call-back, as a result of the throttle user's agreement to
636     * "steal" the locomotive.
637     * <p>
638     * Reminder: for LocoNet throttles which are not using "expanded slot"
639     * functionality, "steal" really means "share".  For those LocoNet throttles
640     * which are using "expanded slots", "steal" really means "force any other
641     * throttle running that address to drop the loco".
642     *
643     * @param address desired DccLocoAddress
644     * @param decision made by the ThrottleListener, only listening for STEAL
645     * @since 4.9.2
646     */
647    @Override
648    public void responseThrottleDecision(LocoAddress address, ThrottleListener l, ThrottleListener.DecisionType decision) {
649        
650        log.debug("{} decision invoked for address {}",decision,address.getNumber() );
651        
652        if ( decision == ThrottleListener.DecisionType.STEAL ) {
653            // Steal is currently implemented by using the same method
654            // we used to acquire the slot prior to the release of
655            // Digitrax command stations with expanded slots.
656            LocoNetSlot slot;
657            synchronized (this) {
658                slot = slotForAddress.get(address.getNumber());
659            }
660            // Only continue if address is found in a slot
661            if (slot != null) {
662                slot.setIsInitialized(false);
663                commitToAcquireThrottle(slot);
664            } else {
665                log.error("Address {} not found in list of slots", address.getNumber());
666            }
667        } else {
668            log.error("Invalid DecisionType {} for LnThrottleManager.",decision);
669        }
670    }
671
672    /*
673     * Internal class for holding throttleListener/LocoAddress pairs for
674     * outstanding requests.
675     */
676    protected static class ThrottleRequest {
677         private LocoAddress la = null;
678         private boolean tc = false;
679
680         ThrottleRequest(LocoAddress l, boolean control) {
681             la = l;
682             tc = control;
683         }
684
685         public boolean getControl() {
686            return tc;
687         }
688         public LocoAddress getAddress() {
689            return la;
690         }
691
692    }
693
694    private static final Logger log = LoggerFactory.getLogger(LnThrottleManager.class);
695
696}