001package jmri.jmrit.automat;
002
003import java.awt.BorderLayout;
004import java.awt.Dimension;
005import java.beans.PropertyChangeEvent;
006import java.beans.PropertyChangeListener;
007import java.util.concurrent.*;
008import javax.annotation.Nonnull;
009import javax.swing.JButton;
010import javax.swing.JFrame;
011import javax.swing.JTextArea;
012
013import jmri.*;
014import jmri.jmrit.logix.OBlock;
015import jmri.jmrit.logix.Warrant;
016
017/**
018 * Abstract base for user automaton classes, which provide individual bits of
019 * automation.
020 * <p>
021 * Each individual automaton runs in a separate thread, so they can operate
022 * independently. This class handles thread creation and scheduling, and
023 * provides a number of services for the user code.
024 * <p>
025 * Subclasses provide a "handle()" function, which does the needed work, and
026 * optionally a "init()" function. These can use any JMRI resources for input
027 * and output. It should not spin on a condition without explicit wait requests;
028 * it is more efficient to use the explicit wait services when waiting for some
029 * specific condition.
030 * <p>
031 * handle() is executed repeatedly until either the Automate object is halted(),
032 * or it returns "false". Returning "true" will just cause handle() to be
033 * invoked again, so you can cleanly restart the Automaton by returning from
034 * multiple points in the function.
035 * <p>
036 * Since handle() executes outside the GUI thread, it is important that access
037 * to GUI (AWT, Swing) objects be scheduled through the various service
038 * routines.
039 * <p>
040 * Services are provided by public member functions, described below. They must
041 * only be invoked from the init and handle methods, as they must be used in a
042 * delayable thread. If invoked from the GUI thread, for example, the program
043 * will appear to hang. To help ensure this, a warning will be logged if they
044 * are used before the thread starts.
045 * <p>
046 * For general use, e.g. in scripts, the most useful functions are:
047 * <ul>
048 * <li>Wait for a specific number of milliseconds: {@link #waitMsec(int)}
049 * <li>Wait for a specific sensor to be active:
050 * {@link #waitSensorActive(jmri.Sensor)} This is also available in a form that
051 * will wait for any of a group of sensors to be active.
052 * <li>Wait for a specific sensor to be inactive:
053 * {@link #waitSensorInactive(jmri.Sensor)} This is also available in a form
054 * that will wait for any of a group of sensors to be inactive.
055 * <li>Wait for a specific sensor to be in a specific state:
056 * {@link #waitSensorState(jmri.Sensor, int)}
057 * <li>Wait for a specific sensor to change:
058 * {@link #waitSensorChange(int, jmri.Sensor)}
059 * <li>Wait for a specific signal head to show a specific appearance:
060 * {@link #waitSignalHeadState(jmri.SignalHead, int)}
061 * <li>Wait for a specific signal mast to show a specific aspect:
062 * {@link #waitSignalMastState(jmri.SignalMast, String)}
063 * <li>Wait for a specific warrant to change run state:
064 * {@link #waitWarrantRunState(Warrant, int)}
065 * <li>Wait for a specific warrant to enter or leave a specific block:
066 * {@link #waitWarrantBlock(Warrant, String, boolean)}
067 * <li>Wait for a specific warrant to enter the next block or to stop:
068 * {@link #waitWarrantBlockChange(Warrant)}
069 * <li>Set a group of turnouts and wait for them to be consistent (actual
070 * position matches desired position):
071 * {@link #setTurnouts(jmri.Turnout[], jmri.Turnout[])}
072 * <li>Wait for a group of turnouts to be consistent (actually as set):
073 * {@link #waitTurnoutConsistent(jmri.Turnout[])}
074 * <li>Wait for any one of a number of Sensors, Turnouts and/or other objects to
075 * change: {@link #waitChange(jmri.NamedBean[])}
076 * <li>Wait for any one of a number of Sensors, Turnouts and/or other objects to
077 * change, up to a specified time: {@link #waitChange(jmri.NamedBean[], int)}
078 * <li>Obtain a DCC throttle: {@link #getThrottle}
079 * <li>Read a CV from decoder on programming track: {@link #readServiceModeCV}
080 * <li>Write a value to a CV in a decoder on the programming track:
081 * {@link #writeServiceModeCV}
082 * <li>Write a value to a CV in a decoder on the main track:
083 * {@link #writeOpsModeCV}
084 * </ul>
085 * <p>
086 * Although this is named an "Abstract" class, it's actually concrete so scripts
087 * can easily use some of the methods.
088 *
089 * @author Bob Jacobsen Copyright (C) 2003
090 */
091public class AbstractAutomaton implements Runnable {
092
093    public AbstractAutomaton() {
094        String className = this.getClass().getName();
095        int lastdot = className.lastIndexOf(".");
096        setName(className.substring(lastdot + 1, className.length()));
097    }
098
099    public AbstractAutomaton(String name) {
100        setName(name);
101    }
102
103    AutomatSummary summary = AutomatSummary.instance();
104
105    Thread currentThread = null;
106
107    /**
108     * Start this automat processing.
109     * <p>
110     * Overrides the superclass method to do local accounting.
111     */
112    public void start() {
113        if (currentThread != null) {
114            log.error("Start with currentThread not null!");
115        }
116        currentThread = jmri.util.ThreadingUtil.newThread(this, name);
117        currentThread.start();
118        summary.register(this);
119        count = 0;
120    }
121
122    private boolean running = false;
123
124    public boolean isRunning() {
125        return running;
126    }
127
128    /**
129     * Part of the implementation; not for general use.
130     * <p>
131     * This is invoked on currentThread.
132     */
133    @Override
134    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value = "IMSE_DONT_CATCH_IMSE",
135            justification = "get these when stop() issued against thread doing BlockingQueue.take() in waitChange, should remove when stop() reimplemented")
136    public void run() {
137        try {
138            inThread = true;
139            init();
140            // the real processing in the next statement is in handle();
141            // and the loop call is just doing accounting
142            running = true;
143            while (handle()) {
144                count++;
145                summary.loop(this);
146            }
147            log.debug("normal termination, handle() returned false");
148            currentThread = null;
149            done();
150        } catch (ThreadDeath e1) {
151            if (currentThread == null) {
152                log.debug("Received ThreadDeath, likely due to stop()");
153            } else {
154                log.warn("Received ThreadDeath while not stopped", e1);
155            }
156        } catch (IllegalMonitorStateException e2) {
157            if (currentThread == null) {
158                log.debug("Received IllegalMonitorStateException, likely due to stop()");
159            } else {
160                log.warn("Received IllegalMonitorStateException while not stopped", e2);
161            }
162        } catch (Exception e3) {
163            log.warn("Unexpected Exception ends AbstractAutomaton thread", e3);
164        } finally {
165            currentThread = null;
166            done();
167        }
168        running = false;
169    }
170
171    /**
172     * Stop the thread immediately.
173     * <p>
174     * Overrides superclass method to handle local accounting.
175     */
176    @SuppressWarnings("deprecation") // Thread.stop()
177    // AbstractAutomaton objects can be waiting on _lots_ of things, so
178    // we need to find another way to deal with this besides Interrupt
179    public void stop() {
180        log.trace("stop() invoked");
181        if (currentThread == null) {
182            log.error("Stop with currentThread null!");
183            return;
184        }
185
186        Thread stoppingThread = currentThread;
187        currentThread = null;
188
189        try {
190            stoppingThread.stop();
191        } catch (java.lang.ThreadDeath e) {
192            log.error("Exception while in stop(): {}", e.toString());
193        }
194
195        done();
196        // note we don't set running = false here.  It's still running until the run() routine thinks it's not.
197        log.trace("stop() completed");
198    }
199
200    /**
201     * Part of the internal implementation; not for general use.
202     * <p>
203     * Common internal end-time processing
204     */
205    void done() {
206        summary.remove(this);
207    }
208
209    private String name = null;
210
211    private int count;
212
213    /**
214     * Get the number of times the handle routine has executed.
215     * <p>
216     * Used by classes such as {@link jmri.jmrit.automat.monitor} to monitor
217     * progress.
218     *
219     * @return the number of times {@link #handle()} has been called on this
220     *         AbstractAutomation
221     */
222    public int getCount() {
223        return count;
224    }
225
226    /**
227     * Get the thread name. Used by classes monitoring this AbstractAutomation,
228     * such as {@link jmri.jmrit.automat.monitor}.
229     *
230     * @return the name of this thread
231     */
232    public String getName() {
233        return name;
234    }
235
236    /**
237     * Update the name of this object.
238     * <p>
239     * name is not a bound parameter, so changes are not notified to listeners.
240     *
241     * @param name the new name
242     * @see #getName()
243     */
244    public void setName(String name) {
245        this.name = name;
246    }
247
248    void defaultName() {
249    }
250
251    /**
252     * User-provided initialization routine.
253     * <p>
254     * This is called exactly once for each object created. This is where you
255     * put all the code that needs to be run when your object starts up: Finding
256     * sensors and turnouts, getting a throttle, etc.
257     */
258    protected void init() {
259    }
260
261    /**
262     * User-provided main routine.
263     * <p>
264     * This is run repeatedly until it signals the end by returning false. Many
265     * automata are intended to run forever, and will always return true.
266     *
267     * @return false to terminate the automaton, for example due to an error.
268     */
269    protected boolean handle() {
270        return false;
271    }
272
273    /**
274     * Control optional debugging prompt. If this is set true, each call to
275     * wait() will prompt the user whether to continue.
276     */
277    protected boolean promptOnWait = false;
278
279    /**
280     * Wait for a specified time and then return control.
281     *
282     * @param milliseconds the number of milliseconds to wait
283     */
284    public void waitMsec(int milliseconds) {
285        long target = System.currentTimeMillis() + milliseconds;
286        while (true) {
287            long stillToGo = target - System.currentTimeMillis();
288            if (stillToGo <= 0) {
289                break;
290            }
291            try {
292                Thread.sleep(stillToGo);
293            } catch (InterruptedException e) {
294                Thread.currentThread().interrupt(); // retain if needed later
295            }
296        }
297    }
298
299    private boolean waiting = false;
300
301    /**
302     * Indicates that object is waiting on a waitSomething call.
303     * <p>
304     * Specifically, the wait has progressed far enough that any change to the
305     * waited-on-condition will be detected.
306     *
307     * @return true if waiting; false otherwise
308     */
309    public boolean isWaiting() {
310        return waiting;
311    }
312
313    /**
314     * Internal common routine to handle start-of-wait bookkeeping.
315     */
316    final private void startWait() {
317        waiting = true;
318    }
319
320    /**
321     * Internal common routine to handle end-of-wait bookkeeping.
322     */
323    final private void endWait() {
324        if (promptOnWait) {
325            debuggingWait();
326        }
327        waiting = false;
328    }
329
330    /**
331     * Part of the internal implementation, not intended for users.
332     * <p>
333     * This handles exceptions internally, so they needn't clutter up the code.
334     * Note that the current implementation doesn't guarantee the time, either
335     * high or low.
336     * <p>
337     * Because of the way Jython access handles synchronization, this is
338     * explicitly synchronized internally.
339     *
340     * @param milliseconds the number of milliseconds to wait
341     */
342    protected void wait(int milliseconds) {
343        startWait();
344        synchronized (this) {
345            try {
346                if (milliseconds < 0) {
347                    super.wait();
348                } else {
349                    super.wait(milliseconds);
350                }
351            } catch (InterruptedException e) {
352                Thread.currentThread().interrupt(); // retain if needed later
353                log.warn("interrupted in wait");
354            }
355        }
356        endWait();
357    }
358
359    /**
360     * Flag used to ensure that service routines are only invoked in the
361     * automaton thread.
362     */
363    private boolean inThread = false;
364
365    private final AbstractAutomaton self = this;
366
367    /**
368     * Wait for a sensor to change state.
369     * <p>
370     * The current (OK) state of the Sensor is passed to avoid a possible race
371     * condition. The new state is returned for a similar reason.
372     * <p>
373     * This works by registering a listener, which is likely to run in another
374     * thread. That listener then interrupts the automaton's thread, who
375     * confirms the change.
376     *
377     * @param mState  Current state of the sensor
378     * @param mSensor Sensor to watch
379     * @return newly detected Sensor state
380     */
381    public int waitSensorChange(int mState, Sensor mSensor) {
382        if (!inThread) {
383            log.warn("waitSensorChange invoked from invalid context");
384        }
385        if (log.isDebugEnabled()) {
386            log.debug("waitSensorChange starts: {}", mSensor.getSystemName());
387        }
388        // register a listener
389        PropertyChangeListener l;
390        mSensor.addPropertyChangeListener(l = (PropertyChangeEvent e) -> {
391            synchronized (self) {
392                self.notifyAll(); // should be only one thread waiting, but just in case
393            }
394        });
395
396        int now;
397        while (mState == (now = mSensor.getKnownState())) {
398            wait(-1);
399        }
400
401        // remove the listener & report new state
402        mSensor.removePropertyChangeListener(l);
403
404        return now;
405    }
406
407    /**
408     * Wait for a sensor to be active. (Returns immediately if already active)
409     *
410     * @param mSensor Sensor to watch
411     */
412    public void waitSensorActive(Sensor mSensor) {
413        if (log.isDebugEnabled()) {
414            log.debug("waitSensorActive starts");
415        }
416        waitSensorState(mSensor, Sensor.ACTIVE);
417    }
418
419    /**
420     * Wait for a sensor to be inactive. (Returns immediately if already
421     * inactive)
422     *
423     * @param mSensor Sensor to watch
424     */
425    public void waitSensorInactive(Sensor mSensor) {
426        if (log.isDebugEnabled()) {
427            log.debug("waitSensorInActive starts");
428        }
429        waitSensorState(mSensor, Sensor.INACTIVE);
430    }
431
432    /**
433     * Internal service routine to wait for one sensor to be in (or become in) a
434     * specific state.
435     * <p>
436     * Used by waitSensorActive and waitSensorInactive
437     * <p>
438     * This works by registering a listener, which is likely to run in another
439     * thread. That listener then interrupts this thread to confirm the change.
440     *
441     * @param mSensor the sensor to wait for
442     * @param state   the expected state
443     */
444    public synchronized void waitSensorState(Sensor mSensor, int state) {
445        if (!inThread) {
446            log.warn("waitSensorState invoked from invalid context");
447        }
448        if (mSensor.getKnownState() == state) {
449            return;
450        }
451        if (log.isDebugEnabled()) {
452            log.debug("waitSensorState starts: {} {}", mSensor.getSystemName(), state);
453        }
454        // register a listener
455        PropertyChangeListener l;
456        mSensor.addPropertyChangeListener(l = (PropertyChangeEvent e) -> {
457            synchronized (self) {
458                self.notifyAll(); // should be only one thread waiting, but just in case
459            }
460        });
461
462        while (state != mSensor.getKnownState()) {
463            wait(-1);  // wait for notification
464        }
465
466        // remove the listener & report new state
467        mSensor.removePropertyChangeListener(l);
468
469    }
470
471    /**
472     * Wait for one of a list of sensors to be be inactive.
473     *
474     * @param mSensors sensors to wait on
475     */
476    public void waitSensorInactive(@Nonnull Sensor[] mSensors) {
477        log.debug("waitSensorInactive[] starts");
478        waitSensorState(mSensors, Sensor.INACTIVE);
479    }
480
481    /**
482     * Wait for one of a list of sensors to be be active.
483     *
484     * @param mSensors sensors to wait on
485     */
486    public void waitSensorActive(@Nonnull Sensor[] mSensors) {
487        log.debug("waitSensorActive[] starts");
488        waitSensorState(mSensors, Sensor.ACTIVE);
489    }
490
491    /**
492     * Wait for one of a list of sensors to be be in a selected state.
493     * <p>
494     * This works by registering a listener, which is likely to run in another
495     * thread. That listener then interrupts the automaton's thread, who
496     * confirms the change.
497     *
498     * @param mSensors Array of sensors to watch
499     * @param state    State to check (static value from jmri.Sensors)
500     */
501    public synchronized void waitSensorState(@Nonnull Sensor[] mSensors, int state) {
502        if (!inThread) {
503            log.warn("waitSensorState invoked from invalid context");
504        }
505        log.debug("waitSensorState[] starts");
506
507        // do a quick check first, just in case
508        if (checkForState(mSensors, state)) {
509            log.debug("returns immediately");
510            return;
511        }
512        // register listeners
513        int i;
514        PropertyChangeListener[] listeners
515                = new PropertyChangeListener[mSensors.length];
516        for (i = 0; i < mSensors.length; i++) {
517
518            mSensors[i].addPropertyChangeListener(listeners[i] = (PropertyChangeEvent e) -> {
519                synchronized (self) {
520                    log.trace("notify waitSensorState[] of property change");
521                    self.notifyAll(); // should be only one thread waiting, but just in case
522                }
523            });
524
525        }
526
527        while (!checkForState(mSensors, state)) {
528            wait(-1);
529        }
530
531        // remove the listeners
532        for (i = 0; i < mSensors.length; i++) {
533            mSensors[i].removePropertyChangeListener(listeners[i]);
534        }
535
536    }
537
538    /**
539     * Internal service routine to wait for one SignalHead to be in (or become in) a
540     * specific state.
541     * <p>
542     * This works by registering a listener, which is likely to run in another
543     * thread. That listener then interrupts this thread to confirm the change.
544     *
545     * @param mSignalHead the signal head to wait for
546     * @param state   the expected state
547     */
548    public synchronized void waitSignalHeadState(SignalHead mSignalHead, int state) {
549        if (!inThread) {
550            log.warn("waitSignalHeadState invoked from invalid context");
551        }
552        if (mSignalHead.getAppearance() == state) {
553            return;
554        }
555        if (log.isDebugEnabled()) {
556            log.debug("waitSignalHeadState starts: {} {}", mSignalHead.getSystemName(), state);
557        }
558        // register a listener
559        PropertyChangeListener l;
560        mSignalHead.addPropertyChangeListener(l = (PropertyChangeEvent e) -> {
561            synchronized (self) {
562                self.notifyAll(); // should be only one thread waiting, but just in case
563            }
564        });
565
566        while (state != mSignalHead.getAppearance()) {
567            wait(-1);  // wait for notification
568        }
569
570        // remove the listener & report new state
571        mSignalHead.removePropertyChangeListener(l);
572
573    }
574
575    /**
576     * Internal service routine to wait for one signal mast to be showing a specific aspect
577     * <p>
578     * This works by registering a listener, which is likely to run in another
579     * thread. That listener then interrupts this thread to confirm the change.
580     *
581     * @param mSignalMast the mast to wait for
582     * @param aspect   the expected aspect
583     */
584    public synchronized void waitSignalMastState(SignalMast mSignalMast, String aspect) {
585        if (!inThread) {
586            log.warn("waitSignalMastState invoked from invalid context");
587        }
588        if (mSignalMast.getAspect().equals(aspect)) {
589            return;
590        }
591        if (log.isDebugEnabled()) {
592            log.debug("waitSignalMastState starts: {} {}", mSignalMast.getSystemName(), aspect);
593        }
594        // register a listener
595        PropertyChangeListener l;
596        mSignalMast.addPropertyChangeListener(l = (PropertyChangeEvent e) -> {
597            synchronized (self) {
598                self.notifyAll(); // should be only one thread waiting, but just in case
599            }
600        });
601
602        while (! mSignalMast.getAspect().equals(aspect)) {
603            wait(-1);  // wait for notification
604        }
605
606        // remove the listener & report new state
607        mSignalMast.removePropertyChangeListener(l);
608
609    }
610
611    /**
612     * Wait for a warrant to change into or out of running state.
613     * <p>
614     * This works by registering a listener, which is likely to run in another
615     * thread. That listener then interrupts the automaton's thread, who
616     * confirms the change.
617     *
618     * @param warrant The name of the warrant to watch
619     * @param state   State to check (static value from jmri.logix.warrant)
620     */
621    public synchronized void waitWarrantRunState(@Nonnull Warrant warrant, int state) {
622        if (!inThread) {
623            log.warn("waitWarrantRunState invoked from invalid context");
624        }
625        if (log.isDebugEnabled()) {
626            log.debug("waitWarrantRunState {}, {} starts", warrant.getDisplayName(), state);
627        }
628
629        // do a quick check first, just in case
630        if (warrant.getRunMode() == state) {
631            log.debug("waitWarrantRunState returns immediately");
632            return;
633        }
634        // register listener
635        PropertyChangeListener listener;
636        warrant.addPropertyChangeListener(listener = (PropertyChangeEvent e) -> {
637            synchronized (self) {
638                log.trace("notify waitWarrantRunState of property change");
639                self.notifyAll(); // should be only one thread waiting, but just in case
640            }
641        });
642
643        while (warrant.getRunMode() != state) {
644            wait(-1);
645        }
646
647        // remove the listener
648        warrant.removePropertyChangeListener(listener);
649
650    }
651
652    /**
653     * Wait for a warrant to enter a named block.
654     * <p>
655     * This works by registering a listener, which is likely to run in another
656     * thread. That listener then interrupts this thread to confirm the change.
657     *
658     * @param warrant  The name of the warrant to watch
659     * @param block    block to check
660     * @param occupied Determines whether to wait for the block to become
661     *                 occupied or unoccupied
662     */
663    public synchronized void waitWarrantBlock(@Nonnull Warrant warrant, @Nonnull String block, boolean occupied) {
664        if (!inThread) {
665            log.warn("waitWarrantBlock invoked from invalid context");
666        }
667        if (log.isDebugEnabled()) {
668            log.debug("waitWarrantBlock {}, {} {} starts", warrant.getDisplayName(), block, occupied);
669        }
670
671        // do a quick check first, just in case
672        if (warrant.getCurrentBlockName().equals(block) == occupied) {
673            log.debug("waitWarrantBlock returns immediately");
674            return;
675        }
676        // register listener
677        PropertyChangeListener listener;
678        warrant.addPropertyChangeListener(listener = (PropertyChangeEvent e) -> {
679            synchronized (self) {
680                log.trace("notify waitWarrantBlock of property change");
681                self.notifyAll(); // should be only one thread waiting, but just in case
682            }
683        });
684
685        while (warrant.getCurrentBlockName().equals(block) != occupied) {
686            wait(-1);
687        }
688
689        // remove the listener
690        warrant.removePropertyChangeListener(listener);
691
692    }
693
694    private volatile boolean blockChanged = false;
695    private volatile String blockName = null;
696
697    /**
698     * Wait for a warrant to either enter a new block or to stop running.
699     * <p>
700     * This works by registering a listener, which is likely to run in another
701     * thread. That listener then interrupts the automaton's thread, who
702     * confirms the change.
703     *
704     * @param warrant The name of the warrant to watch
705     *
706     * @return The name of the block that was entered or null if the warrant is
707     *         no longer running.
708     */
709    public synchronized String waitWarrantBlockChange(@Nonnull Warrant warrant) {
710        if (!inThread) {
711            log.warn("waitWarrantBlockChange invoked from invalid context");
712        }
713        if (log.isDebugEnabled()) {
714            log.debug("waitWarrantBlockChange {}", warrant.getDisplayName());
715        }
716
717        // do a quick check first, just in case
718        if (warrant.getRunMode() != Warrant.MODE_RUN) {
719            log.debug("waitWarrantBlockChange returns immediately");
720            return null;
721        }
722        // register listeners
723        blockName = null;
724        blockChanged = false;
725
726        PropertyChangeListener listener;
727        warrant.addPropertyChangeListener(listener = (PropertyChangeEvent e) -> {
728            if (e.getPropertyName().equals("blockChange")) {
729                blockChanged = true;
730                blockName = ((OBlock) e.getNewValue()).getDisplayName();
731            }
732            if (e.getPropertyName().equals("StopWarrant")) {
733                blockName = null;
734                blockChanged = true;
735            }
736            synchronized (self) {
737                log.trace("notify waitWarrantBlockChange of property change");
738                self.notifyAll(); // should be only one thread waiting, but just in case
739            }
740        });
741
742        while (!blockChanged) {
743            wait(-1);
744        }
745
746        // remove the listener
747        warrant.removePropertyChangeListener(listener);
748
749        return blockName;
750    }
751
752    /**
753     * Wait for a list of turnouts to all be in a consistent state
754     * <p>
755     * This works by registering a listener, which is likely to run in another
756     * thread. That listener then interrupts the automaton's thread, who
757     * confirms the change.
758     *
759     * @param mTurnouts list of turnouts to watch
760     */
761    public synchronized void waitTurnoutConsistent(@Nonnull Turnout[] mTurnouts) {
762        if (!inThread) {
763            log.warn("waitTurnoutConsistent invoked from invalid context");
764        }
765        if (log.isDebugEnabled()) {
766            log.debug("waitTurnoutConsistent[] starts");
767        }
768
769        // do a quick check first, just in case
770        if (checkForConsistent(mTurnouts)) {
771            log.debug("returns immediately");
772            return;
773        }
774        // register listeners
775        int i;
776        PropertyChangeListener[] listeners
777                = new PropertyChangeListener[mTurnouts.length];
778        for (i = 0; i < mTurnouts.length; i++) {
779
780            mTurnouts[i].addPropertyChangeListener(listeners[i] = (PropertyChangeEvent e) -> {
781                synchronized (self) {
782                    log.trace("notify waitTurnoutConsistent[] of property change");
783                    self.notifyAll(); // should be only one thread waiting, but just in case
784                }
785            });
786
787        }
788
789        while (!checkForConsistent(mTurnouts)) {
790            wait(-1);
791        }
792
793        // remove the listeners
794        for (i = 0; i < mTurnouts.length; i++) {
795            mTurnouts[i].removePropertyChangeListener(listeners[i]);
796        }
797
798    }
799
800    /**
801     * Convenience function to set a bunch of turnouts and wait until they are
802     * all in a consistent state
803     *
804     * @param closed turnouts to set to closed state
805     * @param thrown turnouts to set to thrown state
806     */
807    public void setTurnouts(@Nonnull Turnout[] closed, @Nonnull Turnout[] thrown) {
808        Turnout[] turnouts = new Turnout[closed.length + thrown.length];
809        int ti = 0;
810        for (int i = 0; i < closed.length; ++i) {
811            turnouts[ti++] = closed[i];
812            closed[i].setCommandedState(Turnout.CLOSED);
813        }
814        for (int i = 0; i < thrown.length; ++i) {
815            turnouts[ti++] = thrown[i];
816            thrown[i].setCommandedState(Turnout.THROWN);
817        }
818        waitTurnoutConsistent(turnouts);
819    }
820
821    /**
822     * Wait, up to a specified time, for one of a list of NamedBeans (sensors,
823     * signal heads and/or turnouts) to change their state.
824     * <p>
825     * Registers a listener on each of the NamedBeans listed. The listener is
826     * likely to run in another thread. Each fired listener then queues a check
827     * to the automaton's thread.
828     *
829     * @param mInputs  Array of NamedBeans to watch
830     * @param maxDelay maximum amount of time (milliseconds) to wait before
831     *                 continuing anyway. -1 means forever
832     */
833    public void waitChange(@Nonnull NamedBean[] mInputs, int maxDelay) {
834        if (!inThread) {
835            log.warn("waitChange invoked from invalid context");
836        }
837
838        int i;
839        int[] tempState = waitChangePrecheckStates;
840        // do we need to create it now?
841        boolean recreate = false;
842        if (waitChangePrecheckBeans != null && waitChangePrecheckStates != null) {
843            // Seems precheck intended, see if done right
844            if (waitChangePrecheckBeans.length != mInputs.length) {
845                log.warn("Precheck ignored because of mismatch in size: before {}, now {}", waitChangePrecheckBeans.length, mInputs.length);
846                recreate = true;
847            }
848            if (waitChangePrecheckBeans.length != waitChangePrecheckStates.length) {
849                log.error("Precheck data inconsistent because of mismatch in size: {}, {}", waitChangePrecheckBeans.length, waitChangePrecheckStates.length);
850                recreate = true;
851            }
852            if (!recreate) { // have to check if the beans are the same, but only check if the above tests pass
853                for (i = 0; i < mInputs.length; i++) {
854                    if (waitChangePrecheckBeans[i] != mInputs[i]) {
855                        log.warn("Precheck ignored because of mismatch in bean {}", i);
856                        recreate = true;
857                        break;
858                    }
859                }
860            }
861        } else {
862            recreate = true;
863        }
864
865        if (recreate) {
866            // here, have to create a new state array
867            log.trace("recreate state array");
868            tempState = new int[mInputs.length];
869            for (i = 0; i < mInputs.length; i++) {
870                tempState[i] = mInputs[i].getState();
871            }
872        }
873        waitChangePrecheckBeans = null;
874        waitChangePrecheckStates = null;
875        final int[] initialState = tempState; // needs to be final for off-thread references
876
877        log.debug("waitChange[] starts for {} listeners", mInputs.length);
878        waitChangeQueue.clear();
879
880        // register listeners
881        PropertyChangeListener[] listeners = new PropertyChangeListener[mInputs.length];
882        for (i = 0; i < mInputs.length; i++) {
883            mInputs[i].addPropertyChangeListener(listeners[i] = (PropertyChangeEvent e) -> {
884                if (!waitChangeQueue.offer(e)) {
885                    log.warn("Waiting changes capacity exceeded; not adding {} to queue", e);
886                }
887            });
888
889        }
890
891        log.trace("waitChange[] listeners registered");
892
893        // queue a check for whether there was a change while registering
894        jmri.util.ThreadingUtil.runOnLayoutEventually(() -> {
895            log.trace("start separate waitChange check");
896            for (int j = 0; j < mInputs.length; j++) {
897                if (initialState[j] != mInputs[j].getState()) {
898                    log.trace("notify that input {} changed when initial on-layout check was finally done", j);
899                    PropertyChangeEvent e = new PropertyChangeEvent(mInputs[j], "State", initialState[j], mInputs[j].getState());
900                    if (!waitChangeQueue.offer(e)) {
901                        log.warn("Waiting changes capacity exceeded; not adding {} to queue", e);
902                    }
903                    break;
904                }
905            }
906            log.trace("end separate waitChange check");
907        });
908
909        // wait for notify from a listener
910        startWait();
911
912        PropertyChangeEvent prompt;
913        try {
914            if (maxDelay < 0) {
915                prompt = waitChangeQueue.take();
916            } else {
917                prompt = waitChangeQueue.poll(maxDelay, TimeUnit.MILLISECONDS);
918            }
919            if (prompt != null) {
920                log.trace("waitChange continues from {}", prompt.getSource());
921            } else {
922                log.trace("waitChange continues");
923            }
924        } catch (InterruptedException e) {
925            Thread.currentThread().interrupt(); // retain if needed later
926            log.warn("AbstractAutomaton {} waitChange interrupted", getName());
927        }
928
929        // remove the listeners
930        for (i = 0; i < mInputs.length; i++) {
931            mInputs[i].removePropertyChangeListener(listeners[i]);
932        }
933        log.trace("waitChange[] listeners removed");
934        endWait();
935    }
936
937    NamedBean[] waitChangePrecheckBeans = null;
938    int[] waitChangePrecheckStates = null;
939    BlockingQueue<PropertyChangeEvent> waitChangeQueue = new LinkedBlockingQueue<PropertyChangeEvent>();
940
941    /**
942     * Wait forever for one of a list of NamedBeans (sensors, signal heads
943     * and/or turnouts) to change, or for a specific time to pass.
944     *
945     * @param mInputs Array of NamedBeans to watch
946     */
947    public void waitChangePrecheck(NamedBean[] mInputs) {
948        waitChangePrecheckBeans = new NamedBean[mInputs.length];
949        waitChangePrecheckStates = new int[mInputs.length];
950        for (int i = 0; i < mInputs.length; i++) {
951            waitChangePrecheckBeans[i] = mInputs[i];
952            waitChangePrecheckStates[i] = mInputs[i].getState();
953        }
954    }
955
956    /**
957     * Wait forever for one of a list of NamedBeans (sensors, signal heads
958     * and/or turnouts) to change, or for a specific time to pass.
959     *
960     * @param mInputs Array of NamedBeans to watch
961     */
962    public void waitChange(NamedBean[] mInputs) {
963        waitChange(mInputs, -1);
964    }
965
966    /**
967     * Wait for one of an array of sensors to change.
968     * <p>
969     * This is an older method, now superceded by waitChange, which can wait for
970     * any NamedBean.
971     *
972     * @param mSensors Array of sensors to watch
973     */
974    public void waitSensorChange(Sensor[] mSensors) {
975        waitChange(mSensors);
976    }
977
978    /**
979     * Check an array of sensors to see if any are in a specific state
980     *
981     * @param mSensors Array to check
982     * @return true if any are ACTIVE
983     */
984    private boolean checkForState(Sensor[] mSensors, int state) {
985        for (Sensor mSensor : mSensors) {
986            if (mSensor.getKnownState() == state) {
987                return true;
988            }
989        }
990        return false;
991    }
992
993    private boolean checkForConsistent(Turnout[] mTurnouts) {
994        for (int i = 0; i < mTurnouts.length; ++i) {
995            if (!mTurnouts[i].isConsistentState()) {
996                return false;
997            }
998        }
999        return true;
1000    }
1001
1002    private DccThrottle throttle;
1003    private boolean failedThrottleRequest = false;
1004
1005    /**
1006     * Obtains a DCC throttle, including waiting for the command station
1007     * response.
1008     *
1009     * @param address     Numeric address value
1010     * @param longAddress true if this is a long address, false for a short
1011     *                    address
1012     * @param waitSecs    number of seconds to wait for throttle to acquire
1013     *                    before returning null
1014     * @return A usable throttle, or null if error
1015     */
1016    public DccThrottle getThrottle(int address, boolean longAddress, int waitSecs) {
1017        log.debug("requesting DccThrottle for addr {}", address);
1018        if (!inThread) {
1019            log.warn("getThrottle invoked from invalid context");
1020        }
1021        throttle = null;
1022        ThrottleListener throttleListener = new ThrottleListener() {
1023            @Override
1024            public void notifyThrottleFound(DccThrottle t) {
1025                throttle = t;
1026                synchronized (self) {
1027                    self.notifyAll(); // should be only one thread waiting, but just in case
1028                }
1029            }
1030
1031            @Override
1032            public void notifyFailedThrottleRequest(jmri.LocoAddress address, String reason) {
1033                log.error("Throttle request failed for {} because {}", address, reason);
1034                failedThrottleRequest = true;
1035                synchronized (self) {
1036                    self.notifyAll(); // should be only one thread waiting, but just in case
1037                }
1038            }
1039
1040            /**
1041             * No steal or share decisions made locally
1042             */
1043            @Override
1044            public void notifyDecisionRequired(jmri.LocoAddress address, DecisionType question) {
1045            }
1046        };
1047        boolean ok = InstanceManager.getDefault(ThrottleManager.class).requestThrottle(
1048            new jmri.DccLocoAddress(address, longAddress), throttleListener, false);
1049
1050        // check if reply is coming
1051        if (!ok) {
1052            log.info("Throttle for loco {} not available",address);
1053            InstanceManager.getDefault(ThrottleManager.class).cancelThrottleRequest(
1054                new jmri.DccLocoAddress(address, longAddress), throttleListener);  //kill the pending request
1055            return null;
1056        }
1057
1058        // now wait for reply from identified throttle
1059        int waited = 0;
1060        while (throttle == null && failedThrottleRequest == false && waited <= waitSecs) {
1061            log.debug("waiting for throttle");
1062            wait(1000);  //  1 seconds
1063            waited++;
1064            if (throttle == null) {
1065                log.warn("Still waiting for throttle {}!", address);
1066            }
1067        }
1068        if (throttle == null) {
1069            log.debug("canceling request for Throttle {}", address);
1070            InstanceManager.getDefault(ThrottleManager.class).cancelThrottleRequest(
1071                new jmri.DccLocoAddress(address, longAddress), throttleListener);  //kill the pending request
1072        }
1073        return throttle;
1074    }
1075
1076    public DccThrottle getThrottle(int address, boolean longAddress) {
1077        return getThrottle(address, longAddress, 30);  //default to 30 seconds wait
1078    }
1079
1080    /**
1081     * Obtains a DCC throttle, including waiting for the command station
1082     * response.
1083     *
1084     * @param re       specifies the desired locomotive
1085     * @param waitSecs number of seconds to wait for throttle to acquire before
1086     *                 returning null
1087     * @return A usable throttle, or null if error
1088     */
1089    public DccThrottle getThrottle(BasicRosterEntry re, int waitSecs) {
1090        log.debug("requesting DccThrottle for rosterEntry {}", re.getId());
1091        if (!inThread) {
1092            log.warn("getThrottle invoked from invalid context");
1093        }
1094        throttle = null;
1095        ThrottleListener throttleListener = new ThrottleListener() {
1096            @Override
1097            public void notifyThrottleFound(DccThrottle t) {
1098                throttle = t;
1099                synchronized (self) {
1100                    self.notifyAll(); // should be only one thread waiting, but just in case
1101                }
1102            }
1103
1104            @Override
1105            public void notifyFailedThrottleRequest(jmri.LocoAddress address, String reason) {
1106                log.error("Throttle request failed for {} because {}", address, reason);
1107                failedThrottleRequest = true;
1108                synchronized (self) {
1109                    self.notifyAll(); // should be only one thread waiting, but just in case
1110                }
1111            }
1112
1113            /**
1114             * No steal or share decisions made locally
1115             * {@inheritDoc}
1116             */
1117            @Override
1118            public void notifyDecisionRequired(jmri.LocoAddress address, DecisionType question) {
1119            }
1120        };
1121        boolean ok = InstanceManager.getDefault(ThrottleManager.class)
1122                .requestThrottle(re, throttleListener, false);
1123
1124        // check if reply is coming
1125        if (!ok) {
1126            log.info("Throttle for loco {} not available", re.getId());
1127            InstanceManager.getDefault(ThrottleManager.class).cancelThrottleRequest(
1128                re.getDccLocoAddress(), throttleListener);  //kill the pending request
1129            return null;
1130        }
1131
1132        // now wait for reply from identified throttle
1133        int waited = 0;
1134        while (throttle == null && failedThrottleRequest == false && waited <= waitSecs) {
1135            log.debug("waiting for throttle");
1136            wait(1000);  //  1 seconds
1137            waited++;
1138            if (throttle == null) {
1139                log.warn("Still waiting for throttle {}!", re.getId());
1140            }
1141        }
1142        if (throttle == null) {
1143            log.debug("canceling request for Throttle {}", re.getId());
1144            InstanceManager.getDefault(ThrottleManager.class).cancelThrottleRequest(
1145                re.getDccLocoAddress(), throttleListener);  //kill the pending request
1146        }
1147        return throttle;
1148    }
1149
1150    public DccThrottle getThrottle(BasicRosterEntry re) {
1151        return getThrottle(re, 30);  //default to 30 seconds
1152    }
1153
1154    /**
1155     * Write a CV on the service track, including waiting for completion.
1156     *
1157     * @param CV    Number 1 through 512
1158     * @param value Value 0-255 to be written
1159     * @return true if completed OK
1160     */
1161    public boolean writeServiceModeCV(String CV, int value) {
1162        // get service mode programmer
1163        Programmer programmer = InstanceManager.getDefault(jmri.GlobalProgrammerManager.class)
1164                .getGlobalProgrammer();
1165
1166        if (programmer == null) {
1167            log.error("No programmer available as JMRI is currently configured");
1168            return false;
1169        }
1170
1171        // do the write, response will wake the thread
1172        try {
1173            programmer.writeCV(CV, value, (int value1, int status) -> {
1174                synchronized (self) {
1175                    self.notifyAll(); // should be only one thread waiting, but just in case
1176                }
1177            });
1178        } catch (ProgrammerException e) {
1179            log.warn("Exception during writeServiceModeCV", e);
1180            return false;
1181        }
1182        // wait for the result
1183        wait(-1);
1184
1185        return true;
1186    }
1187
1188    private volatile int cvReturnValue;
1189
1190    /**
1191     * Read a CV on the service track, including waiting for completion.
1192     *
1193     * @param CV Number 1 through 512
1194     * @return -1 if error, else value
1195     */
1196    public int readServiceModeCV(String CV) {
1197        // get service mode programmer
1198        Programmer programmer = InstanceManager.getDefault(jmri.GlobalProgrammerManager.class)
1199                .getGlobalProgrammer();
1200
1201        if (programmer == null) {
1202            log.error("No programmer available as JMRI is currently configured");
1203            return -1;
1204        }
1205
1206        // do the read, response will wake the thread
1207        cvReturnValue = -1;
1208        try {
1209            programmer.readCV(CV, (int value, int status) -> {
1210                cvReturnValue = value;
1211                synchronized (self) {
1212                    self.notifyAll(); // should be only one thread waiting, but just in case
1213                }
1214            });
1215        } catch (ProgrammerException e) {
1216            log.warn("Exception during writeServiceModeCV", e);
1217            return -1;
1218        }
1219        // wait for the result
1220        wait(-1);
1221        return cvReturnValue;
1222    }
1223
1224    /**
1225     * Write a CV in ops mode, including waiting for completion.
1226     *
1227     * @param CV          Number 1 through 512
1228     * @param value       0-255 value to be written
1229     * @param loco        Locomotive decoder address
1230     * @param longAddress true is the locomotive is using a long address
1231     * @return true if completed OK
1232     */
1233    public boolean writeOpsModeCV(String CV, int value, boolean longAddress, int loco) {
1234        // get service mode programmer
1235        Programmer programmer = InstanceManager.getDefault(jmri.AddressedProgrammerManager.class)
1236                .getAddressedProgrammer(longAddress, loco);
1237
1238        if (programmer == null) {
1239            log.error("No programmer available as JMRI is currently configured");
1240            return false;
1241        }
1242
1243        // do the write, response will wake the thread
1244        try {
1245            programmer.writeCV(CV, value, (int value1, int status) -> {
1246                synchronized (self) {
1247                    self.notifyAll(); // should be only one thread waiting, but just in case
1248                }
1249            });
1250        } catch (ProgrammerException e) {
1251            log.warn("Exception during writeServiceModeCV", e);
1252            return false;
1253        }
1254        // wait for the result
1255        wait(-1);
1256
1257        return true;
1258    }
1259
1260    JFrame messageFrame = null;
1261    String message = null;
1262
1263    /**
1264     * Internal class to show a Frame
1265     */
1266    public class MsgFrame implements Runnable {
1267
1268        String mMessage;
1269        boolean mPause;
1270        boolean mShow;
1271        JFrame mFrame = null;
1272        JButton mButton;
1273        JTextArea mArea;
1274
1275        public void hide() {
1276            mShow = false;
1277            // invoke the operation
1278            javax.swing.SwingUtilities.invokeLater(this);
1279        }
1280
1281        /**
1282         * Show a message in the message frame, and optionally wait for the user
1283         * to acknowledge.
1284         *
1285         * @param pMessage the message to show
1286         * @param pPause   true if this automaton should wait for user
1287         *                 acknowledgment; false otherwise
1288         */
1289        public void show(String pMessage, boolean pPause) {
1290            mMessage = pMessage;
1291            mPause = pPause;
1292            mShow = true;
1293
1294            // invoke the operation
1295            javax.swing.SwingUtilities.invokeLater(this);
1296            // wait to proceed?
1297            if (mPause) {
1298                synchronized (self) {
1299                    new jmri.util.WaitHandler(this);
1300                }
1301            }
1302        }
1303
1304        @Override
1305        public void run() {
1306            // create the frame if it doesn't exist
1307            if (mFrame == null) {
1308                mFrame = new JFrame("");
1309                mArea = new JTextArea();
1310                mArea.setEditable(false);
1311                mArea.setLineWrap(false);
1312                mArea.setWrapStyleWord(true);
1313                mButton = new JButton("Continue");
1314                mFrame.getContentPane().setLayout(new BorderLayout());
1315                mFrame.getContentPane().add(mArea, BorderLayout.CENTER);
1316                mFrame.getContentPane().add(mButton, BorderLayout.SOUTH);
1317                mButton.addActionListener((java.awt.event.ActionEvent e) -> {
1318                    synchronized (self) {
1319                        self.notifyAll(); // should be only one thread waiting, but just in case
1320                    }
1321                    mFrame.setVisible(false);
1322                });
1323                mFrame.pack();
1324            }
1325            if (mShow) {
1326                // update message, show button if paused
1327                mArea.setText(mMessage);
1328                if (mPause) {
1329                    mButton.setVisible(true);
1330                } else {
1331                    mButton.setVisible(false);
1332                }
1333                // do optional formatting
1334                format();
1335                // center the frame
1336                mFrame.pack();
1337                Dimension screen = mFrame.getContentPane().getToolkit().getScreenSize();
1338                Dimension size = mFrame.getSize();
1339                mFrame.setLocation((screen.width - size.width) / 2, (screen.height - size.height) / 2);
1340                // and show it to the user
1341                mFrame.setVisible(true);
1342            } else {
1343                mFrame.setVisible(false);
1344            }
1345        }
1346
1347        /**
1348         * Abstract method to handle formatting of the text on a show
1349         */
1350        protected void format() {
1351        }
1352    }
1353
1354    JFrame debugWaitFrame = null;
1355
1356    /**
1357     * Wait for the user to OK moving forward. This is complicated by not
1358     * running in the GUI thread, and by not wanting to use a modal dialog.
1359     */
1360    private void debuggingWait() {
1361        // post an event to the GUI pane
1362        Runnable r = () -> {
1363            // create a prompting frame
1364            if (debugWaitFrame == null) {
1365                debugWaitFrame = new JFrame("Automaton paused");
1366                JButton b = new JButton("Continue");
1367                debugWaitFrame.getContentPane().add(b);
1368                b.addActionListener((java.awt.event.ActionEvent e) -> {
1369                    synchronized (self) {
1370                        self.notifyAll(); // should be only one thread waiting, but just in case
1371                    }
1372                    debugWaitFrame.setVisible(false);
1373                });
1374                debugWaitFrame.pack();
1375            }
1376            debugWaitFrame.setVisible(true);
1377        };
1378        javax.swing.SwingUtilities.invokeLater(r);
1379        // wait to proceed
1380        try {
1381            super.wait();
1382        } catch (InterruptedException e) {
1383            Thread.currentThread().interrupt(); // retain if needed later
1384            log.warn("Interrupted during debugging wait, not expected");
1385        }
1386    }
1387    // initialize logging
1388    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(AbstractAutomaton.class);
1389}