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