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        if (requestOutstanding) {
079           try {
080              // queue this request for later.
081              requestList.put(new ThrottleRequest(address,control));
082           } catch (InterruptedException ie) {
083              log.error("Interrupted while trying to store throttle request");
084              requestOutstanding = false;
085           }
086        } else {
087           // handle this now
088           requestOutstanding = true;
089           processThrottleSetupRequest(address, control);
090        }
091     }
092
093    /**
094     * Processes the next loco from the queue of requested locos for which to get
095     * a LocoNetThrottle.
096     */
097    protected void processQueuedThrottleSetupRequest() {
098        if (!requestOutstanding && (requestList.size() != 0 )) {
099           requestOutstanding = true;
100           try {
101              ThrottleRequest tr = requestList.take();
102              processThrottleSetupRequest(tr.getAddress(), tr.getControl());
103           } catch (InterruptedException ie) {
104              log.error("Interrupted while trying to process process throttle request");
105              requestOutstanding = false;
106           }
107        }
108     }
109
110    /**
111     * Begin the processing of a Throttle Request.
112     *
113     * @param address Loco address
114     * @param control whether the throttle object wants to control the loco
115     */
116    private void processThrottleSetupRequest(LocoAddress address, boolean control) {
117        slotManager.slotFromLocoAddress(address.getNumber(), this);  //first try
118
119        class RetrySetup implements Runnable { // setup for retries and failure check
120
121            final DccLocoAddress address;
122            final SlotListener list;
123
124            RetrySetup(DccLocoAddress address, SlotListener list) {
125                this.address = address;
126                this.list = list;
127            }
128
129            @Override
130            public void run() {
131                int attempts = 1; // already tried once above
132                int maxAttempts = 10;
133                while (attempts <= maxAttempts) {
134                    try {
135                        Thread.sleep(1000); // wait one second
136                    } catch (InterruptedException ex) {
137                        return; // stop waiting if slot is found or error occurs
138                    }
139                    String again = "";
140                    if (attempts < maxAttempts) {
141                        slotManager.slotFromLocoAddress(address.getNumber(), list);
142                        again = ", trying again."; // NOI18N
143                    }
144                    log.debug("No response to slot request for {}, attempt {} {}", address, attempts, again);
145                    attempts++;
146                }
147                log.error("No response to slot request for {} after {} attempts.", address, attempts - 1); // NOI18N
148                failedThrottleRequest(address, "Failed to get response from command station");
149                requestOutstanding = false;
150                processQueuedThrottleSetupRequest();
151            }
152        }
153
154        retrySetupThread = new Thread(
155                new RetrySetup(new DccLocoAddress(address.getNumber(),
156                        isLongAddress(address.getNumber())), this));
157        retrySetupThread.setName("LnThrottleManager RetrySetup " + address);
158        retrySetupThread.start();
159        synchronized (this) {
160            waitingForNotification.put(address.getNumber(), retrySetupThread);
161        }
162    }
163
164    volatile Thread retrySetupThread;
165
166    Hashtable<Integer, Thread> waitingForNotification = new Hashtable<>(5);
167
168    Hashtable<Integer, LocoNetSlot> slotForAddress;
169    LinkedBlockingQueue<ThrottleRequest> requestList;
170    boolean requestOutstanding = false;
171
172    /**
173     * LocoNet does have a Dispatch function.
174     *
175     * @return true
176     */
177    @Override
178    public boolean hasDispatchFunction() {
179        return true;
180    }
181
182    /**
183     * What speed modes are supported by this system? value should be xor of
184     * possible modes specified by the DccThrottle interface.
185     *
186     * @return an integer containing the combined speed step modes supported
187     */
188    @Override
189    public EnumSet<SpeedStepMode> supportedSpeedModes() {
190        return EnumSet.of(SpeedStepMode.NMRA_DCC_128
191                , SpeedStepMode.NMRA_DCC_28
192                , SpeedStepMode.MOTOROLA_28
193                , SpeedStepMode.NMRA_DCC_14);
194    }
195
196    /**
197     * Get notification that an address has changed slot. This method creates a
198     * throttle for all ThrottleListeners of that address and notifies them via
199     * the ThrottleListener.notifyThrottleFound method.
200     *
201     * @param s LocoNet slot which has been changed
202     */
203    @Override
204    public void notifyChangedSlot(LocoNetSlot s) {
205        log.debug("notifyChangedSlot - slot {}, slotStatus {}", s.getSlot(), Integer.toHexString(s.slotStatus()));
206        // This is invoked only if the SlotManager knows that the LnThrottleManager is
207        // interested in the address associated with this slot.
208
209        // need to check to see if the slot is in a suitable state for creating a throttle.
210        if (s.slotStatus() == LnConstants.LOCO_IN_USE) {
211            // loco is already in-use
212            log.warn("slot {} address {} is already in-use.",
213                    s.getSlot(), s.locoAddr());
214            // is the throttle ID the same as for this JMRI instance?  If not, do not accept the slot.
215            if ((s.id() != 0) && s.id() != throttleID) {
216                // notify the LnThrottleManager about failure of acquisition.
217                // NEED TO TRIGGER THE NEW "STEAL REQUIRED" FUNCTIONALITY HERE
218                //note: throttle listener expects to have "callback" method notifyDecisionRequired
219                //invoked if a "steal" is required.  Make that happen as part of the "acquisition" process
220                synchronized (this) {
221                    slotForAddress.put(s.locoAddr(), s);
222                }
223                notifyStealRequest(s.locoAddr());
224                return;
225            }
226            // shared throttle / already ours
227            notifyComplete(commitToAcquireThrottle(s),s);
228            return;
229        }
230        commitToAcquireThrottle(s);
231    }
232
233    /**
234     * Making progress in the process of acquiring a throttle.
235     *
236     * @param s slot to be acquired
237     */
238    private DccThrottle commitToAcquireThrottle(LocoNetSlot s) {
239        // haven't identified a particular reason to refuse throttle acquisition at this time...
240        return createThrottle((LocoNetSystemConnectionMemo) adapterMemo, s);
241        // the rest is done when the write of the throttle ID has been acknowledged  in the throttle
242        // by calling notifyComplete
243     }
244
245    /**
246     * Called from the throttle slot when the final write of throttle id has been
247     * completed, and the slot is set as initialized, or called directly for our own shared throttles.
248     * @param t the throttle
249     * @param s the lot.
250     */
251     protected void notifyComplete(DccThrottle t, LocoNetSlot s) {
252         // end the waiting thread since we got a response
253         s.notifySlotListeners(); // make sure other listeners for this slot
254                                  // know about what's going on!
255         notifyThrottleKnown(t, new DccLocoAddress(s.locoAddr(), isLongAddress(s.locoAddr())));
256         synchronized (this) {
257             if (waitingForNotification.containsKey(s.locoAddr())) {
258                 log.debug(
259                         "LnThrottleManager.notifyChangedSlot() - removing throttle acquisition notification flagging for address {}",
260                         s.locoAddr());
261                 waitingForNotification.get(s.locoAddr()).interrupt();
262                 waitingForNotification.remove(s.locoAddr());
263             } else {
264                 log.debug(
265                         "LnThrottleManager.notifyChangedSlot() - ignoring slot notification for slot {}, address {} account not attempting to acquire that address",
266                         s.getSlot(), s.locoAddr());
267             }
268             slotForAddress.remove(s.locoAddr());
269         }
270         requestOutstanding = false;
271         processQueuedThrottleSetupRequest();
272     }
273
274    /**
275     * Loco acquisition failed. Propagate the failure message to the (GUI)
276     * throttle.
277     *
278     * @param address of the loco which could not be acquired
279     * @param cause reason for the failure
280     */
281    public void notifyRefused(int address, String cause) {
282        //end the waiting thread since we got a failure response
283        synchronized (this) {
284            if (waitingForNotification.containsKey(address)) {
285                waitingForNotification.get(address).interrupt();
286                waitingForNotification.remove(address);
287                // notify the throttle - in some other thread!
288
289                class InformRejection implements Runnable {
290                    // inform the throttle from a new thread, so that
291                    // the modal dialog box doesn't block other LocoNet
292                    // message handling
293
294                    final int address;
295                    final String cause;
296
297                    InformRejection(int address, String s) {
298                        this.address = address;
299                        this.cause = s;
300                    }
301
302                    @Override
303                    public void run() {
304
305                        log.debug("New thread launched to inform throttle user of failure to acquire loco {} - {}", address, cause);
306                        failedThrottleRequest(new DccLocoAddress(address, isLongAddress(address)), cause);
307                    }
308
309                }
310                Thread thr = new Thread(new InformRejection(address, cause));
311                thr.start();
312            }
313            slotForAddress.remove(address);
314        }
315        requestOutstanding = false;
316        processQueuedThrottleSetupRequest();
317    }
318
319
320    /**
321     * Create a LocoNet Throttle to control a loco.
322     * <p>
323     * This is called during the loco acquisition process by logic within
324     * LnThrottleManager.  Generally, it should not be directly called by other
325     * methods.
326     *
327     * @param memo connection memo used by the throttle for communications
328     * @param s slot holding an acquired loco
329     * @return throttle holding an acquired loco
330     */
331    DccThrottle createThrottle(LocoNetSystemConnectionMemo memo, LocoNetSlot s) {
332        log.debug("createThrottle: slot {}", s.getSlot());
333        return new LocoNetThrottle(memo, s);
334    }
335
336    /**
337     * Determines if the loco address is a long address.
338     * <p>
339     * For LocoNet, address 128 and above is a long address.
340     *
341     * @param address to be checked
342     * @return true if long address, else false
343     */
344    @Override
345    public boolean canBeLongAddress(int address) {
346        return isLongAddress(address);
347    }
348
349    /**
350     * Determines if the loco address is a short address.
351     * <p>
352     * For LocoNet, address 127 and below is a short address
353     *
354     * @param address to be checked
355     * @return true if short address, else false
356     */
357    @Override
358    public boolean canBeShortAddress(int address) {
359        return !isLongAddress(address);
360    }
361
362    /**
363     * Reports whether all loco addresses are uniquely long or short, without any
364     * ambiguity for any address.
365     * <p>
366     * For LocoNet, there are no ambiguous addresses.
367     *
368     * @return true
369     */
370    @Override
371    public boolean addressTypeUnique() {
372        return true;
373    }
374
375    /**
376     * Local method for deciding short/long address.
377     *
378     * @param num address to be checked
379     * @return true if num is a long address else false
380     */
381    protected static boolean isLongAddress(int num) {
382        return (num >= 128);
383    }
384
385    /**
386     * Disposes a LnThrottle object.
387     * <p>
388     * Generally, this will cause the slot to be made "common" and the LnThrottle
389     * is disposed of.
390     * <p>
391     * After disposal, the throttle may not be used to control the loco.
392     *
393     * @param t is a throttle to be disposed of
394     * @param l is the listener for the throttle
395     * @return false if throttle is not a LocoNetThrottle, else true
396     */
397    @Override
398    public boolean disposeThrottle(DccThrottle t, ThrottleListener l) {
399        log.debug("disposeThrottle - throttle {}", t.getLocoAddress());
400        if (t instanceof LocoNetThrottle) {
401            if (super.disposeThrottle(t, l)) {
402                LocoNetThrottle lnt = (LocoNetThrottle) t;
403                lnt.throttleDispose();
404                return true;
405            }
406        }
407        return false;
408    }
409
410    /**
411     * Dispatches a loco from a LnThrottle object.
412     * <p>
413     * Generally, this will cause the slot to be made "common" and then linked via
414     * the "Dispatch" slot.
415     * <p>
416     * After dispatching, the throttle may not be used to control the loco.
417     * You should check getUsageCountBefore calling as it will fail if not 1.
418     *
419     * @param t is a throttle to be disposed of
420     * @param l is the listener for the throttle
421     */
422    @Override
423    public void dispatchThrottle(DccThrottle t, ThrottleListener l) {
424        log.debug("dispatchThrottle - throttle {}", t.getLocoAddress());
425        // Use slot to dispatch, then release
426        if (t instanceof LocoNetThrottle) {
427            // only dispatch if its the last throttle use
428            if (super.getThrottleUsageCount(t.getLocoAddress()) == 1)  {
429                ((LocoNetThrottle) t).dispatchThrottle(t, l);
430            } else {
431                return;
432            }
433        }
434        super.releaseThrottle(t, l);
435    }
436
437    /**
438     * Dispatch a loco from a LnThrottle object.
439     * <p>
440     * Generally, this will cause the slot to be made "common".
441     * <p>
442     * After disposal, the throttle may not be used to control the loco.
443     *
444     * @param t is a throttle to be disposed of
445     * @param l is the listener for the throttle
446     */
447    @Override
448    public void releaseThrottle(DccThrottle t, ThrottleListener l) {
449        log.debug("releaseThrottle - throttle {}", t.getLocoAddress());
450        super.releaseThrottle(t, l);
451    }
452
453    /**
454     * Cancels the loco acquisition process when throttle acquisition of a loco
455     * fails.
456     *
457     * @param address loco address which could not be acquired
458     * @param reason for the failure
459     */
460    @Override
461    public void failedThrottleRequest(LocoAddress address, String reason) {
462        super.failedThrottleRequest(address, reason);
463        log.debug("failedThrottleRequest - address {}, reason {}", address, reason);
464        //now end and remove any waiting thread
465        synchronized (this) {
466            if (waitingForNotification.containsKey(address.getNumber())) {
467                waitingForNotification.get(address.getNumber()).interrupt();
468                waitingForNotification.remove(address.getNumber());
469            }
470            slotForAddress.remove(address.getNumber());
471        }
472        requestOutstanding = false;
473        processQueuedThrottleSetupRequest();
474    }
475
476    /**
477     * Cancel a request for a throttle.
478     *
479     * @param address The decoder address desired.
480     *                address.
481     * @param l       The ThrottleListener cancelling request for a throttle.
482     */
483    @Override
484    public void cancelThrottleRequest(LocoAddress address, ThrottleListener l) {
485        
486        // calling super removes the ThrottleListener from the callback list,
487        // The listener which has just sent the cancel doesn't need notification
488        // of the cancel but other listeners might
489        super.cancelThrottleRequest(address, l);
490        
491        failedThrottleRequest(address, "Throttle Request " + address + " Cancelled.");
492        
493        int loconumber = address.getNumber();
494        log.debug("cancelThrottleRequest - loconumber {}", loconumber);
495        synchronized (this) {
496            if (waitingForNotification.containsKey(loconumber)) {
497                waitingForNotification.get(loconumber).interrupt();
498                waitingForNotification.remove(loconumber);
499            }
500            slotForAddress.remove(loconumber);
501        }
502        requestOutstanding = false;
503        processQueuedThrottleSetupRequest();
504    }
505
506    protected int throttleID = 0x0171;
507
508    /**
509     * Get the ThrottleID value for this throttle.
510     *
511     * @return the ThrottleID value
512     */
513    public int getThrottleID() {
514        return throttleID;
515    }
516
517    /**
518     * {@inheritDoc}
519     * Dispose of this manager, typically for testing.
520     */
521    @Override
522    public void dispose() {
523        if (retrySetupThread != null) {
524            try {
525                retrySetupThread.interrupt();
526                retrySetupThread.join();
527            } catch (InterruptedException ex) {
528                log.warn("dispose interrupted");
529            }
530        }
531    }
532
533    /**
534     * Inform the requesting throttle object (not the connection-specific throttle
535     * implementation!)  that the address is in-use and the throttle user may
536     * either choose to "steal" the address, or quit the acquisition process.
537     * The LocoNet acquisition process "retry" timer is stopped as part of this
538     * process, since a positive response has been received from the command station
539     * and since user intervention is required.
540     *
541     * Reminder: for LocoNet throttles which are not using "expanded slot"
542     * functionality, "steal" really means "share".  For those LocoNet throttles
543     * which are using "expanded slots", "steal" really means take control and
544     * let the command station issue a "StealZap" LocoNet message to the other throttle.
545     *
546     * @param locoAddr address of DCC loco or consist
547     */
548    public void notifyStealRequest(int locoAddr) {
549        // need to find the "throttleListener" associated with the request for locoAddr, and
550        // send that "throttleListener" a notification that the command station needs
551        // permission to "steal" the loco address.
552        synchronized (this) {
553            if (waitingForNotification.containsKey(locoAddr)) {
554                waitingForNotification.get(locoAddr).interrupt();
555                waitingForNotification.remove(locoAddr);
556
557                notifyDecisionRequest(new DccLocoAddress(locoAddr, isLongAddress(locoAddr)), ThrottleListener.DecisionType.STEAL);
558            }
559        }
560    }
561
562    /**
563     * Perform the actual "Steal" of the requested throttle.
564     * <p>
565     * This is a call-back, as a result of the throttle user's agreement to
566     * "steal" the locomotive.
567     * <p>
568     * Reminder: for LocoNet throttles which are not using "expanded slot"
569     * functionality, "steal" really means "share".  For those LocoNet throttles
570     * which are using "expanded slots", "steal" really means "force any other
571     * throttle running that address to drop the loco".
572     *
573     * @param address desired DccLocoAddress
574     * @param decision made by the ThrottleListener, only listening for STEAL
575     * @since 4.9.2
576     */
577    @Override
578    public void responseThrottleDecision(LocoAddress address, ThrottleListener l, ThrottleListener.DecisionType decision) {
579        
580        log.debug("{} decision invoked for address {}",decision,address.getNumber() );
581        
582        if ( decision == ThrottleListener.DecisionType.STEAL ) {
583            // Steal is currently implemented by using the same method
584            // we used to acquire the slot prior to the release of
585            // Digitrax command stations with expanded slots.
586            LocoNetSlot slot;
587            synchronized (this) {
588                slot = slotForAddress.get(address.getNumber());
589            }
590            // Only continue if address is found in a slot
591            if (slot != null) {
592                slot.setIsInitialized(false);
593                commitToAcquireThrottle(slot);
594            } else {
595                log.error("Address {} not found in list of slots", address.getNumber());
596            }
597        } else {
598            log.error("Invalid DecisionType {} for LnThrottleManager.",decision);
599        }
600    }
601
602    /*
603     * Internal class for holding throttleListener/LocoAddress pairs for
604     * outstanding requests.
605     */
606    protected static class ThrottleRequest {
607         private LocoAddress la = null;
608         private boolean tc = false;
609
610         ThrottleRequest(LocoAddress l, boolean control) {
611             la = l;
612             tc = control;
613         }
614
615         public boolean getControl() {
616            return tc;
617         }
618         public LocoAddress getAddress() {
619            return la;
620         }
621
622    }
623
624    private final static Logger log = LoggerFactory.getLogger(LnThrottleManager.class);
625
626}