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 * Wait forever for one of a list of NamedBeans (sensors, signal heads 914 * and/or turnouts) to change, or for a specific time to pass. 915 * 916 * @param mInputs Array of NamedBeans to watch 917 */ 918 public void waitChangePrecheck(NamedBean[] mInputs) { 919 waitChangePrecheckBeans = new NamedBean[mInputs.length]; 920 waitChangePrecheckStates = new int[mInputs.length]; 921 for (int i = 0; i < mInputs.length; i++) { 922 waitChangePrecheckBeans[i] = mInputs[i]; 923 waitChangePrecheckStates[i] = mInputs[i].getState(); 924 } 925 } 926 927 /** 928 * Wait forever for one of a list of NamedBeans (sensors, signal heads 929 * and/or turnouts) to change, or for a specific time to pass. 930 * 931 * @param mInputs Array of NamedBeans to watch 932 */ 933 public void waitChange(NamedBean[] mInputs) { 934 waitChange(mInputs, -1); 935 } 936 937 /** 938 * Wait for one of an array of sensors to change. 939 * <p> 940 * This is an older method, now superceded by waitChange, which can wait for 941 * any NamedBean. 942 * 943 * @param mSensors Array of sensors to watch 944 */ 945 public void waitSensorChange(Sensor[] mSensors) { 946 waitChange(mSensors); 947 } 948 949 /** 950 * Check an array of sensors to see if any are in a specific state 951 * 952 * @param mSensors Array to check 953 * @return true if any are ACTIVE 954 */ 955 private boolean checkForState(Sensor[] mSensors, int state) { 956 for (Sensor mSensor : mSensors) { 957 if (mSensor.getKnownState() == state) { 958 return true; 959 } 960 } 961 return false; 962 } 963 964 private boolean checkForConsistent(Turnout[] mTurnouts) { 965 for (int i = 0; i < mTurnouts.length; ++i) { 966 if (!mTurnouts[i].isConsistentState()) { 967 return false; 968 } 969 } 970 return true; 971 } 972 973 private DccThrottle throttle; 974 private boolean failedThrottleRequest = false; 975 976 /** 977 * Obtains a DCC throttle, including waiting for the command station 978 * response. 979 * 980 * @param address Numeric address value 981 * @param longAddress true if this is a long address, false for a short 982 * address 983 * @param waitSecs number of seconds to wait for throttle to acquire 984 * before returning null 985 * @return A usable throttle, or null if error 986 */ 987 public DccThrottle getThrottle(int address, boolean longAddress, int waitSecs) { 988 log.debug("requesting DccThrottle for addr {}", address); 989 if (!inThread) { 990 log.warn("getThrottle invoked from invalid context"); 991 } 992 throttle = null; 993 ThrottleListener throttleListener = new ThrottleListener() { 994 @Override 995 public void notifyThrottleFound(DccThrottle t) { 996 throttle = t; 997 synchronized (self) { 998 self.notifyAll(); // should be only one thread waiting, but just in case 999 } 1000 } 1001 1002 @Override 1003 public void notifyFailedThrottleRequest(jmri.LocoAddress address, String reason) { 1004 log.error("Throttle request failed for {} because {}", address, reason); 1005 failedThrottleRequest = true; 1006 synchronized (self) { 1007 self.notifyAll(); // should be only one thread waiting, but just in case 1008 } 1009 } 1010 1011 /** 1012 * No steal or share decisions made locally 1013 */ 1014 @Override 1015 public void notifyDecisionRequired(jmri.LocoAddress address, DecisionType question) { 1016 } 1017 }; 1018 boolean ok = InstanceManager.getDefault(ThrottleManager.class).requestThrottle( 1019 new jmri.DccLocoAddress(address, longAddress), throttleListener, false); 1020 1021 // check if reply is coming 1022 if (!ok) { 1023 log.info("Throttle for loco {} not available",address); 1024 InstanceManager.getDefault(ThrottleManager.class).cancelThrottleRequest( 1025 new jmri.DccLocoAddress(address, longAddress), throttleListener); //kill the pending request 1026 return null; 1027 } 1028 1029 // now wait for reply from identified throttle 1030 int waited = 0; 1031 while (throttle == null && failedThrottleRequest == false && waited <= waitSecs) { 1032 log.debug("waiting for throttle"); 1033 wait(1000); // 1 seconds 1034 waited++; 1035 if (throttle == null) { 1036 log.warn("Still waiting for throttle {}!", address); 1037 } 1038 } 1039 if (throttle == null) { 1040 log.debug("canceling request for Throttle {}", address); 1041 InstanceManager.getDefault(ThrottleManager.class).cancelThrottleRequest( 1042 new jmri.DccLocoAddress(address, longAddress), throttleListener); //kill the pending request 1043 } 1044 return throttle; 1045 } 1046 1047 public DccThrottle getThrottle(int address, boolean longAddress) { 1048 return getThrottle(address, longAddress, 30); //default to 30 seconds wait 1049 } 1050 1051 /** 1052 * Obtains a DCC throttle, including waiting for the command station 1053 * response. 1054 * 1055 * @param re specifies the desired locomotive 1056 * @param waitSecs number of seconds to wait for throttle to acquire before 1057 * returning null 1058 * @return A usable throttle, or null if error 1059 */ 1060 public DccThrottle getThrottle(BasicRosterEntry re, int waitSecs) { 1061 log.debug("requesting DccThrottle for rosterEntry {}", re.getId()); 1062 if (!inThread) { 1063 log.warn("getThrottle invoked from invalid context"); 1064 } 1065 throttle = null; 1066 ThrottleListener throttleListener = new ThrottleListener() { 1067 @Override 1068 public void notifyThrottleFound(DccThrottle t) { 1069 throttle = t; 1070 synchronized (self) { 1071 self.notifyAll(); // should be only one thread waiting, but just in case 1072 } 1073 } 1074 1075 @Override 1076 public void notifyFailedThrottleRequest(jmri.LocoAddress address, String reason) { 1077 log.error("Throttle request failed for {} because {}", address, reason); 1078 failedThrottleRequest = true; 1079 synchronized (self) { 1080 self.notifyAll(); // should be only one thread waiting, but just in case 1081 } 1082 } 1083 1084 /** 1085 * No steal or share decisions made locally 1086 * {@inheritDoc} 1087 */ 1088 @Override 1089 public void notifyDecisionRequired(jmri.LocoAddress address, DecisionType question) { 1090 } 1091 }; 1092 boolean ok = InstanceManager.getDefault(ThrottleManager.class) 1093 .requestThrottle(re, throttleListener, false); 1094 1095 // check if reply is coming 1096 if (!ok) { 1097 log.info("Throttle for loco {} not available", re.getId()); 1098 InstanceManager.getDefault(ThrottleManager.class).cancelThrottleRequest( 1099 re.getDccLocoAddress(), throttleListener); //kill the pending request 1100 return null; 1101 } 1102 1103 // now wait for reply from identified throttle 1104 int waited = 0; 1105 while (throttle == null && failedThrottleRequest == false && waited <= waitSecs) { 1106 log.debug("waiting for throttle"); 1107 wait(1000); // 1 seconds 1108 waited++; 1109 if (throttle == null) { 1110 log.warn("Still waiting for throttle {}!", re.getId()); 1111 } 1112 } 1113 if (throttle == null) { 1114 log.debug("canceling request for Throttle {}", re.getId()); 1115 InstanceManager.getDefault(ThrottleManager.class).cancelThrottleRequest( 1116 re.getDccLocoAddress(), throttleListener); //kill the pending request 1117 } 1118 return throttle; 1119 } 1120 1121 public DccThrottle getThrottle(BasicRosterEntry re) { 1122 return getThrottle(re, 30); //default to 30 seconds 1123 } 1124 1125 /** 1126 * Write a CV on the service track, including waiting for completion. 1127 * 1128 * @param cv Number 1 through 512 1129 * @param value Value 0-255 to be written 1130 * @return true if completed OK 1131 */ 1132 public boolean writeServiceModeCV(String cv, int value) { 1133 // get service mode programmer 1134 Programmer programmer = InstanceManager.getDefault(jmri.GlobalProgrammerManager.class) 1135 .getGlobalProgrammer(); 1136 1137 if (programmer == null) { 1138 log.error("No programmer available as JMRI is currently configured"); 1139 return false; 1140 } 1141 1142 // do the write, response will wake the thread 1143 try { 1144 programmer.writeCV(cv, value, (int value1, int status) -> { 1145 synchronized (self) { 1146 self.notifyAll(); // should be only one thread waiting, but just in case 1147 } 1148 }); 1149 } catch (ProgrammerException e) { 1150 log.warn("Exception during writeServiceModeCV", e); 1151 return false; 1152 } 1153 // wait for the result 1154 wait(-1); 1155 1156 return true; 1157 } 1158 1159 private volatile int cvReturnValue; 1160 1161 /** 1162 * Read a CV on the service track, including waiting for completion. 1163 * 1164 * @param cv Number 1 through 512 1165 * @return -1 if error, else value 1166 */ 1167 public int readServiceModeCV(String cv) { 1168 // get service mode programmer 1169 Programmer programmer = InstanceManager.getDefault(jmri.GlobalProgrammerManager.class) 1170 .getGlobalProgrammer(); 1171 1172 if (programmer == null) { 1173 log.error("No programmer available as JMRI is currently configured"); 1174 return -1; 1175 } 1176 1177 // do the read, response will wake the thread 1178 cvReturnValue = -1; 1179 try { 1180 programmer.readCV(cv, (int value, int status) -> { 1181 cvReturnValue = value; 1182 synchronized (self) { 1183 self.notifyAll(); // should be only one thread waiting, but just in case 1184 } 1185 }); 1186 } catch (ProgrammerException e) { 1187 log.warn("Exception during writeServiceModeCV", e); 1188 return -1; 1189 } 1190 // wait for the result 1191 wait(-1); 1192 return cvReturnValue; 1193 } 1194 1195 /** 1196 * Write a CV in ops mode, including waiting for completion. 1197 * 1198 * @param cv Number 1 through 512 1199 * @param value 0-255 value to be written 1200 * @param loco Locomotive decoder address 1201 * @param longAddress true is the locomotive is using a long address 1202 * @return true if completed OK 1203 */ 1204 public boolean writeOpsModeCV(String cv, int value, boolean longAddress, int loco) { 1205 // get service mode programmer 1206 Programmer programmer = InstanceManager.getDefault(jmri.AddressedProgrammerManager.class) 1207 .getAddressedProgrammer(longAddress, loco); 1208 1209 if (programmer == null) { 1210 log.error("No programmer available as JMRI is currently configured"); 1211 return false; 1212 } 1213 1214 // do the write, response will wake the thread 1215 try { 1216 programmer.writeCV(cv, value, (int value1, int status) -> { 1217 synchronized (self) { 1218 self.notifyAll(); // should be only one thread waiting, but just in case 1219 } 1220 }); 1221 } catch (ProgrammerException e) { 1222 log.warn("Exception during writeServiceModeCV", e); 1223 return false; 1224 } 1225 // wait for the result 1226 wait(-1); 1227 1228 return true; 1229 } 1230 1231 JFrame messageFrame = null; 1232 String message = null; 1233 1234 /** 1235 * Internal class to show a Frame 1236 */ 1237 public class MsgFrame implements Runnable { 1238 1239 String mMessage; 1240 boolean mPause; 1241 boolean mShow; 1242 JFrame mFrame = null; 1243 JButton mButton; 1244 JTextArea mArea; 1245 1246 public void hide() { 1247 mShow = false; 1248 // invoke the operation 1249 javax.swing.SwingUtilities.invokeLater(this); 1250 } 1251 1252 /** 1253 * Show a message in the message frame, and optionally wait for the user 1254 * to acknowledge. 1255 * 1256 * @param pMessage the message to show 1257 * @param pPause true if this automaton should wait for user 1258 * acknowledgment; false otherwise 1259 */ 1260 public void show(String pMessage, boolean pPause) { 1261 mMessage = pMessage; 1262 mPause = pPause; 1263 mShow = true; 1264 1265 // invoke the operation 1266 javax.swing.SwingUtilities.invokeLater(this); 1267 // wait to proceed? 1268 if (mPause) { 1269 synchronized (self) { 1270 new jmri.util.WaitHandler(this); 1271 } 1272 } 1273 } 1274 1275 @Override 1276 public void run() { 1277 // create the frame if it doesn't exist 1278 if (mFrame == null) { 1279 mFrame = new JFrame(""); 1280 mArea = new JTextArea(); 1281 mArea.setEditable(false); 1282 mArea.setLineWrap(false); 1283 mArea.setWrapStyleWord(true); 1284 mButton = new JButton("Continue"); 1285 mFrame.getContentPane().setLayout(new BorderLayout()); 1286 mFrame.getContentPane().add(mArea, BorderLayout.CENTER); 1287 mFrame.getContentPane().add(mButton, BorderLayout.SOUTH); 1288 mButton.addActionListener((java.awt.event.ActionEvent e) -> { 1289 synchronized (self) { 1290 self.notifyAll(); // should be only one thread waiting, but just in case 1291 } 1292 mFrame.setVisible(false); 1293 }); 1294 mFrame.pack(); 1295 } 1296 if (mShow) { 1297 // update message, show button if paused 1298 mArea.setText(mMessage); 1299 if (mPause) { 1300 mButton.setVisible(true); 1301 } else { 1302 mButton.setVisible(false); 1303 } 1304 // do optional formatting 1305 format(); 1306 // center the frame 1307 mFrame.pack(); 1308 Dimension screen = mFrame.getContentPane().getToolkit().getScreenSize(); 1309 Dimension size = mFrame.getSize(); 1310 mFrame.setLocation((screen.width - size.width) / 2, (screen.height - size.height) / 2); 1311 // and show it to the user 1312 mFrame.setVisible(true); 1313 } else { 1314 mFrame.setVisible(false); 1315 } 1316 } 1317 1318 /** 1319 * Abstract method to handle formatting of the text on a show 1320 */ 1321 protected void format() { 1322 } 1323 } 1324 1325 JFrame debugWaitFrame = null; 1326 1327 /** 1328 * Wait for the user to OK moving forward. This is complicated by not 1329 * running in the GUI thread, and by not wanting to use a modal dialog. 1330 */ 1331 private void debuggingWait() { 1332 // post an event to the GUI pane 1333 Runnable r = () -> { 1334 // create a prompting frame 1335 if (debugWaitFrame == null) { 1336 debugWaitFrame = new JFrame("Automaton paused"); 1337 JButton b = new JButton("Continue"); 1338 debugWaitFrame.getContentPane().add(b); 1339 b.addActionListener((java.awt.event.ActionEvent e) -> { 1340 synchronized (self) { 1341 self.notifyAll(); // should be only one thread waiting, but just in case 1342 } 1343 debugWaitFrame.setVisible(false); 1344 }); 1345 debugWaitFrame.pack(); 1346 } 1347 debugWaitFrame.setVisible(true); 1348 }; 1349 javax.swing.SwingUtilities.invokeLater(r); 1350 // wait to proceed 1351 try { 1352 super.wait(); 1353 } catch (InterruptedException e) { 1354 if (threadIsStopped) { 1355 throw new StopThreadException(); 1356 } 1357 Thread.currentThread().interrupt(); // retain if needed later 1358 log.warn("Interrupted during debugging wait, not expected"); 1359 } 1360 } 1361 1362 /** 1363 * An exception that's used internally in AbstractAutomation to stop 1364 * the thread. 1365 */ 1366 private static class StopThreadException extends RuntimeException { 1367 } 1368 1369 // initialize logging 1370 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(AbstractAutomaton.class); 1371}