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