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