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