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