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