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