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     * Remembers the current state of a set of NamedBeans
914     * so that a later looping call to waitChange(..) on that same
915     * list won't miss any intervening changes.
916     *
917     * @param mInputs Array of NamedBeans to watch
918     */
919    public void waitChangePrecheck(NamedBean[] mInputs) {
920        waitChangePrecheckBeans = new NamedBean[mInputs.length];
921        waitChangePrecheckStates = new int[mInputs.length];
922        for (int i = 0; i < mInputs.length; i++) {
923            waitChangePrecheckBeans[i] = mInputs[i];
924            waitChangePrecheckStates[i] = mInputs[i].getState();
925        }
926    }
927
928    /**
929     * Wait forever for one of a list of NamedBeans (sensors, signal heads
930     * and/or turnouts) to change.
931     *
932     * @param mInputs Array of NamedBeans to watch
933     */
934    public void waitChange(NamedBean[] mInputs) {
935        waitChange(mInputs, -1);
936    }
937
938    /**
939     * Wait for one of an array of sensors to change.
940     * <p>
941     * This is an older method, now superceded by waitChange, which can wait for
942     * any NamedBean.
943     *
944     * @param mSensors Array of sensors to watch
945     */
946    public void waitSensorChange(Sensor[] mSensors) {
947        waitChange(mSensors);
948    }
949
950    /**
951     * Check an array of sensors to see if any are in a specific state
952     *
953     * @param mSensors Array to check
954     * @return true if any are ACTIVE
955     */
956    private boolean checkForState(Sensor[] mSensors, int state) {
957        for (Sensor mSensor : mSensors) {
958            if (mSensor.getKnownState() == state) {
959                return true;
960            }
961        }
962        return false;
963    }
964
965    private boolean checkForConsistent(Turnout[] mTurnouts) {
966        for (int i = 0; i < mTurnouts.length; ++i) {
967            if (!mTurnouts[i].isConsistentState()) {
968                return false;
969            }
970        }
971        return true;
972    }
973
974    private DccThrottle throttle;
975    private boolean failedThrottleRequest = false;
976
977    /**
978     * Obtains a DCC throttle, including waiting for the command station
979     * response.
980     *
981     * @param address     Numeric address value
982     * @param longAddress true if this is a long address, false for a short
983     *                    address
984     * @param waitSecs    number of seconds to wait for throttle to acquire
985     *                    before returning null
986     * @return A usable throttle, or null if error
987     */
988    public DccThrottle getThrottle(int address, boolean longAddress, int waitSecs) {
989        log.debug("requesting DccThrottle for addr {}", address);
990        if (!inThread) {
991            log.warn("getThrottle invoked from invalid context");
992        }
993        throttle = null;
994        ThrottleListener throttleListener = new ThrottleListener() {
995            @Override
996            public void notifyThrottleFound(DccThrottle t) {
997                throttle = t;
998                synchronized (self) {
999                    self.notifyAll(); // should be only one thread waiting, but just in case
1000                }
1001            }
1002
1003            @Override
1004            public void notifyFailedThrottleRequest(jmri.LocoAddress address, String reason) {
1005                log.error("Throttle request failed for {} because {}", address, reason);
1006                failedThrottleRequest = true;
1007                synchronized (self) {
1008                    self.notifyAll(); // should be only one thread waiting, but just in case
1009                }
1010            }
1011
1012            /**
1013             * No steal or share decisions made locally
1014             */
1015            @Override
1016            public void notifyDecisionRequired(jmri.LocoAddress address, DecisionType question) {
1017            }
1018        };
1019        boolean ok = InstanceManager.getDefault(ThrottleManager.class).requestThrottle(
1020            new jmri.DccLocoAddress(address, longAddress), throttleListener, false);
1021
1022        // check if reply is coming
1023        if (!ok) {
1024            log.info("Throttle for loco {} not available",address);
1025            InstanceManager.getDefault(ThrottleManager.class).cancelThrottleRequest(
1026                new jmri.DccLocoAddress(address, longAddress), throttleListener);  //kill the pending request
1027            return null;
1028        }
1029
1030        // now wait for reply from identified throttle
1031        int waited = 0;
1032        while (throttle == null && failedThrottleRequest == false && waited <= waitSecs) {
1033            log.debug("waiting for throttle");
1034            wait(1000);  //  1 seconds
1035            waited++;
1036            if (throttle == null) {
1037                log.warn("Still waiting for throttle {}!", address);
1038            }
1039        }
1040        if (throttle == null) {
1041            log.debug("canceling request for Throttle {}", address);
1042            InstanceManager.getDefault(ThrottleManager.class).cancelThrottleRequest(
1043                new jmri.DccLocoAddress(address, longAddress), throttleListener);  //kill the pending request
1044        }
1045        return throttle;
1046    }
1047
1048    public DccThrottle getThrottle(int address, boolean longAddress) {
1049        return getThrottle(address, longAddress, 30);  //default to 30 seconds wait
1050    }
1051
1052    /**
1053     * Obtains a DCC throttle, including waiting for the command station
1054     * response.
1055     *
1056     * @param re       specifies the desired locomotive
1057     * @param waitSecs number of seconds to wait for throttle to acquire before
1058     *                 returning null
1059     * @return A usable throttle, or null if error
1060     */
1061    public DccThrottle getThrottle(BasicRosterEntry re, int waitSecs) {
1062        log.debug("requesting DccThrottle for rosterEntry {}", re.getId());
1063        if (!inThread) {
1064            log.warn("getThrottle invoked from invalid context");
1065        }
1066        throttle = null;
1067        ThrottleListener throttleListener = new ThrottleListener() {
1068            @Override
1069            public void notifyThrottleFound(DccThrottle t) {
1070                throttle = t;
1071                synchronized (self) {
1072                    self.notifyAll(); // should be only one thread waiting, but just in case
1073                }
1074            }
1075
1076            @Override
1077            public void notifyFailedThrottleRequest(jmri.LocoAddress address, String reason) {
1078                log.error("Throttle request failed for {} because {}", address, reason);
1079                failedThrottleRequest = true;
1080                synchronized (self) {
1081                    self.notifyAll(); // should be only one thread waiting, but just in case
1082                }
1083            }
1084
1085            /**
1086             * No steal or share decisions made locally
1087             * {@inheritDoc}
1088             */
1089            @Override
1090            public void notifyDecisionRequired(jmri.LocoAddress address, DecisionType question) {
1091            }
1092        };
1093        boolean ok = InstanceManager.getDefault(ThrottleManager.class)
1094                .requestThrottle(re, throttleListener, false);
1095
1096        // check if reply is coming
1097        if (!ok) {
1098            log.info("Throttle for loco {} not available", re.getId());
1099            InstanceManager.getDefault(ThrottleManager.class).cancelThrottleRequest(
1100                re.getDccLocoAddress(), throttleListener);  //kill the pending request
1101            return null;
1102        }
1103
1104        // now wait for reply from identified throttle
1105        int waited = 0;
1106        while (throttle == null && failedThrottleRequest == false && waited <= waitSecs) {
1107            log.debug("waiting for throttle");
1108            wait(1000);  //  1 seconds
1109            waited++;
1110            if (throttle == null) {
1111                log.warn("Still waiting for throttle {}!", re.getId());
1112            }
1113        }
1114        if (throttle == null) {
1115            log.debug("canceling request for Throttle {}", re.getId());
1116            InstanceManager.getDefault(ThrottleManager.class).cancelThrottleRequest(
1117                re.getDccLocoAddress(), throttleListener);  //kill the pending request
1118        }
1119        return throttle;
1120    }
1121
1122    public DccThrottle getThrottle(BasicRosterEntry re) {
1123        return getThrottle(re, 30);  //default to 30 seconds
1124    }
1125
1126    /**
1127     * Write a CV on the service track, including waiting for completion.
1128     *
1129     * @param cv    Number 1 through 512
1130     * @param value Value 0-255 to be written
1131     * @return true if completed OK
1132     */
1133    public boolean writeServiceModeCV(String cv, int value) {
1134        // get service mode programmer
1135        Programmer programmer = InstanceManager.getDefault(jmri.GlobalProgrammerManager.class)
1136                .getGlobalProgrammer();
1137
1138        if (programmer == null) {
1139            log.error("No programmer available as JMRI is currently configured");
1140            return false;
1141        }
1142
1143        // do the write, response will wake the thread
1144        try {
1145            programmer.writeCV(cv, value, (int value1, int status) -> {
1146                synchronized (self) {
1147                    self.notifyAll(); // should be only one thread waiting, but just in case
1148                }
1149            });
1150        } catch (ProgrammerException e) {
1151            log.warn("Exception during writeServiceModeCV", e);
1152            return false;
1153        }
1154        // wait for the result
1155        wait(-1);
1156
1157        return true;
1158    }
1159
1160    private volatile int cvReturnValue;
1161
1162    /**
1163     * Read a CV on the service track, including waiting for completion.
1164     *
1165     * @param cv Number 1 through 512
1166     * @return -1 if error, else value
1167     */
1168    public int readServiceModeCV(String cv) {
1169        // get service mode programmer
1170        Programmer programmer = InstanceManager.getDefault(jmri.GlobalProgrammerManager.class)
1171                .getGlobalProgrammer();
1172
1173        if (programmer == null) {
1174            log.error("No programmer available as JMRI is currently configured");
1175            return -1;
1176        }
1177
1178        // do the read, response will wake the thread
1179        cvReturnValue = -1;
1180        try {
1181            programmer.readCV(cv, (int value, int status) -> {
1182                cvReturnValue = value;
1183                synchronized (self) {
1184                    self.notifyAll(); // should be only one thread waiting, but just in case
1185                }
1186            });
1187        } catch (ProgrammerException e) {
1188            log.warn("Exception during writeServiceModeCV", e);
1189            return -1;
1190        }
1191        // wait for the result
1192        wait(-1);
1193        return cvReturnValue;
1194    }
1195
1196    /**
1197     * Write a CV in ops mode, including waiting for completion.
1198     *
1199     * @param cv          Number 1 through 512
1200     * @param value       0-255 value to be written
1201     * @param loco        Locomotive decoder address
1202     * @param longAddress true is the locomotive is using a long address
1203     * @return true if completed OK
1204     */
1205    public boolean writeOpsModeCV(String cv, int value, boolean longAddress, int loco) {
1206        // get service mode programmer
1207        Programmer programmer = InstanceManager.getDefault(jmri.AddressedProgrammerManager.class)
1208                .getAddressedProgrammer(longAddress, loco);
1209
1210        if (programmer == null) {
1211            log.error("No programmer available as JMRI is currently configured");
1212            return false;
1213        }
1214
1215        // do the write, response will wake the thread
1216        try {
1217            programmer.writeCV(cv, value, (int value1, int status) -> {
1218                synchronized (self) {
1219                    self.notifyAll(); // should be only one thread waiting, but just in case
1220                }
1221            });
1222        } catch (ProgrammerException e) {
1223            log.warn("Exception during writeServiceModeCV", e);
1224            return false;
1225        }
1226        // wait for the result
1227        wait(-1);
1228
1229        return true;
1230    }
1231
1232    JFrame messageFrame = null;
1233    String message = null;
1234
1235    /**
1236     * Internal class to show a Frame
1237     */
1238    public class MsgFrame implements Runnable {
1239
1240        String mMessage;
1241        boolean mPause;
1242        boolean mShow;
1243        JFrame mFrame = null;
1244        JButton mButton;
1245        JTextArea mArea;
1246
1247        public void hide() {
1248            mShow = false;
1249            // invoke the operation
1250            javax.swing.SwingUtilities.invokeLater(this);
1251        }
1252
1253        /**
1254         * Show a message in the message frame, and optionally wait for the user
1255         * to acknowledge.
1256         *
1257         * @param pMessage the message to show
1258         * @param pPause   true if this automaton should wait for user
1259         *                 acknowledgment; false otherwise
1260         */
1261        public void show(String pMessage, boolean pPause) {
1262            mMessage = pMessage;
1263            mPause = pPause;
1264            mShow = true;
1265
1266            // invoke the operation
1267            javax.swing.SwingUtilities.invokeLater(this);
1268            // wait to proceed?
1269            if (mPause) {
1270                synchronized (self) {
1271                    new jmri.util.WaitHandler(this);
1272                }
1273            }
1274        }
1275
1276        @Override
1277        public void run() {
1278            // create the frame if it doesn't exist
1279            if (mFrame == null) {
1280                mFrame = new JFrame("");
1281                mArea = new JTextArea();
1282                mArea.setEditable(false);
1283                mArea.setLineWrap(false);
1284                mArea.setWrapStyleWord(true);
1285                mButton = new JButton("Continue");
1286                mFrame.getContentPane().setLayout(new BorderLayout());
1287                mFrame.getContentPane().add(mArea, BorderLayout.CENTER);
1288                mFrame.getContentPane().add(mButton, BorderLayout.SOUTH);
1289                mButton.addActionListener((java.awt.event.ActionEvent e) -> {
1290                    synchronized (self) {
1291                        self.notifyAll(); // should be only one thread waiting, but just in case
1292                    }
1293                    mFrame.setVisible(false);
1294                });
1295                mFrame.pack();
1296            }
1297            if (mShow) {
1298                // update message, show button if paused
1299                mArea.setText(mMessage);
1300                if (mPause) {
1301                    mButton.setVisible(true);
1302                } else {
1303                    mButton.setVisible(false);
1304                }
1305                // do optional formatting
1306                format();
1307                // center the frame
1308                mFrame.pack();
1309                Dimension screen = mFrame.getContentPane().getToolkit().getScreenSize();
1310                Dimension size = mFrame.getSize();
1311                mFrame.setLocation((screen.width - size.width) / 2, (screen.height - size.height) / 2);
1312                // and show it to the user
1313                mFrame.setVisible(true);
1314            } else {
1315                mFrame.setVisible(false);
1316            }
1317        }
1318
1319        /**
1320         * Abstract method to handle formatting of the text on a show
1321         */
1322        protected void format() {
1323        }
1324    }
1325
1326    JFrame debugWaitFrame = null;
1327
1328    /**
1329     * Wait for the user to OK moving forward. This is complicated by not
1330     * running in the GUI thread, and by not wanting to use a modal dialog.
1331     */
1332    private void debuggingWait() {
1333        // post an event to the GUI pane
1334        Runnable r = () -> {
1335            // create a prompting frame
1336            if (debugWaitFrame == null) {
1337                debugWaitFrame = new JFrame("Automaton paused");
1338                JButton b = new JButton("Continue");
1339                debugWaitFrame.getContentPane().add(b);
1340                b.addActionListener((java.awt.event.ActionEvent e) -> {
1341                    synchronized (self) {
1342                        self.notifyAll(); // should be only one thread waiting, but just in case
1343                    }
1344                    debugWaitFrame.setVisible(false);
1345                });
1346                debugWaitFrame.pack();
1347            }
1348            debugWaitFrame.setVisible(true);
1349        };
1350        javax.swing.SwingUtilities.invokeLater(r);
1351        // wait to proceed
1352        try {
1353            super.wait();
1354        } catch (InterruptedException e) {
1355            if (threadIsStopped) {
1356                throw new StopThreadException();
1357            }
1358            Thread.currentThread().interrupt(); // retain if needed later
1359            log.warn("Interrupted during debugging wait, not expected");
1360        }
1361    }
1362
1363    /**
1364     * An exception that's used internally in AbstractAutomation to stop
1365     * the thread.
1366     */
1367    private static class StopThreadException extends RuntimeException {
1368    }
1369
1370    // initialize logging
1371    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(AbstractAutomaton.class);
1372}