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 final private void startWait() { 317 waiting = true; 318 } 319 320 /** 321 * Internal common routine to handle end-of-wait bookkeeping. 322 */ 323 final 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 if (log.isDebugEnabled()) { 386 log.debug("waitSensorChange starts: {}", mSensor.getSystemName()); 387 } 388 // register a listener 389 PropertyChangeListener l; 390 mSensor.addPropertyChangeListener(l = (PropertyChangeEvent e) -> { 391 synchronized (self) { 392 self.notifyAll(); // should be only one thread waiting, but just in case 393 } 394 }); 395 396 int now; 397 while (mState == (now = mSensor.getKnownState())) { 398 wait(-1); 399 } 400 401 // remove the listener & report new state 402 mSensor.removePropertyChangeListener(l); 403 404 return now; 405 } 406 407 /** 408 * Wait for a sensor to be active. (Returns immediately if already active) 409 * 410 * @param mSensor Sensor to watch 411 */ 412 public void waitSensorActive(Sensor mSensor) { 413 if (log.isDebugEnabled()) { 414 log.debug("waitSensorActive starts"); 415 } 416 waitSensorState(mSensor, Sensor.ACTIVE); 417 } 418 419 /** 420 * Wait for a sensor to be inactive. (Returns immediately if already 421 * inactive) 422 * 423 * @param mSensor Sensor to watch 424 */ 425 public void waitSensorInactive(Sensor mSensor) { 426 if (log.isDebugEnabled()) { 427 log.debug("waitSensorInActive starts"); 428 } 429 waitSensorState(mSensor, Sensor.INACTIVE); 430 } 431 432 /** 433 * Internal service routine to wait for one sensor to be in (or become in) a 434 * specific state. 435 * <p> 436 * Used by waitSensorActive and waitSensorInactive 437 * <p> 438 * This works by registering a listener, which is likely to run in another 439 * thread. That listener then interrupts this thread to confirm the change. 440 * 441 * @param mSensor the sensor to wait for 442 * @param state the expected state 443 */ 444 public synchronized void waitSensorState(Sensor mSensor, int state) { 445 if (!inThread) { 446 log.warn("waitSensorState invoked from invalid context"); 447 } 448 if (mSensor.getKnownState() == state) { 449 return; 450 } 451 if (log.isDebugEnabled()) { 452 log.debug("waitSensorState starts: {} {}", mSensor.getSystemName(), state); 453 } 454 // register a listener 455 PropertyChangeListener l; 456 mSensor.addPropertyChangeListener(l = (PropertyChangeEvent e) -> { 457 synchronized (self) { 458 self.notifyAll(); // should be only one thread waiting, but just in case 459 } 460 }); 461 462 while (state != mSensor.getKnownState()) { 463 wait(-1); // wait for notification 464 } 465 466 // remove the listener & report new state 467 mSensor.removePropertyChangeListener(l); 468 469 } 470 471 /** 472 * Wait for one of a list of sensors to be be inactive. 473 * 474 * @param mSensors sensors to wait on 475 */ 476 public void waitSensorInactive(@Nonnull Sensor[] mSensors) { 477 log.debug("waitSensorInactive[] starts"); 478 waitSensorState(mSensors, Sensor.INACTIVE); 479 } 480 481 /** 482 * Wait for one of a list of sensors to be be active. 483 * 484 * @param mSensors sensors to wait on 485 */ 486 public void waitSensorActive(@Nonnull Sensor[] mSensors) { 487 log.debug("waitSensorActive[] starts"); 488 waitSensorState(mSensors, Sensor.ACTIVE); 489 } 490 491 /** 492 * Wait for one of a list of sensors to be be in a selected state. 493 * <p> 494 * This works by registering a listener, which is likely to run in another 495 * thread. That listener then interrupts the automaton's thread, who 496 * confirms the change. 497 * 498 * @param mSensors Array of sensors to watch 499 * @param state State to check (static value from jmri.Sensors) 500 */ 501 public synchronized void waitSensorState(@Nonnull Sensor[] mSensors, int state) { 502 if (!inThread) { 503 log.warn("waitSensorState invoked from invalid context"); 504 } 505 log.debug("waitSensorState[] starts"); 506 507 // do a quick check first, just in case 508 if (checkForState(mSensors, state)) { 509 log.debug("returns immediately"); 510 return; 511 } 512 // register listeners 513 int i; 514 PropertyChangeListener[] listeners 515 = new PropertyChangeListener[mSensors.length]; 516 for (i = 0; i < mSensors.length; i++) { 517 518 mSensors[i].addPropertyChangeListener(listeners[i] = (PropertyChangeEvent e) -> { 519 synchronized (self) { 520 log.trace("notify waitSensorState[] of property change"); 521 self.notifyAll(); // should be only one thread waiting, but just in case 522 } 523 }); 524 525 } 526 527 while (!checkForState(mSensors, state)) { 528 wait(-1); 529 } 530 531 // remove the listeners 532 for (i = 0; i < mSensors.length; i++) { 533 mSensors[i].removePropertyChangeListener(listeners[i]); 534 } 535 536 } 537 538 /** 539 * Internal service routine to wait for one SignalHead to be in (or become in) a 540 * specific state. 541 * <p> 542 * This works by registering a listener, which is likely to run in another 543 * thread. That listener then interrupts this thread to confirm the change. 544 * 545 * @param mSignalHead the signal head to wait for 546 * @param state the expected state 547 */ 548 public synchronized void waitSignalHeadState(SignalHead mSignalHead, int state) { 549 if (!inThread) { 550 log.warn("waitSignalHeadState invoked from invalid context"); 551 } 552 if (mSignalHead.getAppearance() == state) { 553 return; 554 } 555 if (log.isDebugEnabled()) { 556 log.debug("waitSignalHeadState starts: {} {}", mSignalHead.getSystemName(), state); 557 } 558 // register a listener 559 PropertyChangeListener l; 560 mSignalHead.addPropertyChangeListener(l = (PropertyChangeEvent e) -> { 561 synchronized (self) { 562 self.notifyAll(); // should be only one thread waiting, but just in case 563 } 564 }); 565 566 while (state != mSignalHead.getAppearance()) { 567 wait(-1); // wait for notification 568 } 569 570 // remove the listener & report new state 571 mSignalHead.removePropertyChangeListener(l); 572 573 } 574 575 /** 576 * Internal service routine to wait for one signal mast to be showing a specific aspect 577 * <p> 578 * This works by registering a listener, which is likely to run in another 579 * thread. That listener then interrupts this thread to confirm the change. 580 * 581 * @param mSignalMast the mast to wait for 582 * @param aspect the expected aspect 583 */ 584 public synchronized void waitSignalMastState(SignalMast mSignalMast, String aspect) { 585 if (!inThread) { 586 log.warn("waitSignalMastState invoked from invalid context"); 587 } 588 if (mSignalMast.getAspect().equals(aspect)) { 589 return; 590 } 591 if (log.isDebugEnabled()) { 592 log.debug("waitSignalMastState starts: {} {}", mSignalMast.getSystemName(), aspect); 593 } 594 // register a listener 595 PropertyChangeListener l; 596 mSignalMast.addPropertyChangeListener(l = (PropertyChangeEvent e) -> { 597 synchronized (self) { 598 self.notifyAll(); // should be only one thread waiting, but just in case 599 } 600 }); 601 602 while (! mSignalMast.getAspect().equals(aspect)) { 603 wait(-1); // wait for notification 604 } 605 606 // remove the listener & report new state 607 mSignalMast.removePropertyChangeListener(l); 608 609 } 610 611 /** 612 * Wait for a warrant to change into or out of running state. 613 * <p> 614 * This works by registering a listener, which is likely to run in another 615 * thread. That listener then interrupts the automaton's thread, who 616 * confirms the change. 617 * 618 * @param warrant The name of the warrant to watch 619 * @param state State to check (static value from jmri.logix.warrant) 620 */ 621 public synchronized void waitWarrantRunState(@Nonnull Warrant warrant, int state) { 622 if (!inThread) { 623 log.warn("waitWarrantRunState invoked from invalid context"); 624 } 625 if (log.isDebugEnabled()) { 626 log.debug("waitWarrantRunState {}, {} starts", warrant.getDisplayName(), state); 627 } 628 629 // do a quick check first, just in case 630 if (warrant.getRunMode() == state) { 631 log.debug("waitWarrantRunState returns immediately"); 632 return; 633 } 634 // register listener 635 PropertyChangeListener listener; 636 warrant.addPropertyChangeListener(listener = (PropertyChangeEvent e) -> { 637 synchronized (self) { 638 log.trace("notify waitWarrantRunState of property change"); 639 self.notifyAll(); // should be only one thread waiting, but just in case 640 } 641 }); 642 643 while (warrant.getRunMode() != state) { 644 wait(-1); 645 } 646 647 // remove the listener 648 warrant.removePropertyChangeListener(listener); 649 650 } 651 652 /** 653 * Wait for a warrant to enter a named block. 654 * <p> 655 * This works by registering a listener, which is likely to run in another 656 * thread. That listener then interrupts this thread to confirm the change. 657 * 658 * @param warrant The name of the warrant to watch 659 * @param block block to check 660 * @param occupied Determines whether to wait for the block to become 661 * occupied or unoccupied 662 */ 663 public synchronized void waitWarrantBlock(@Nonnull Warrant warrant, @Nonnull String block, boolean occupied) { 664 if (!inThread) { 665 log.warn("waitWarrantBlock invoked from invalid context"); 666 } 667 if (log.isDebugEnabled()) { 668 log.debug("waitWarrantBlock {}, {} {} starts", warrant.getDisplayName(), block, occupied); 669 } 670 671 // do a quick check first, just in case 672 if (warrant.getCurrentBlockName().equals(block) == occupied) { 673 log.debug("waitWarrantBlock returns immediately"); 674 return; 675 } 676 // register listener 677 PropertyChangeListener listener; 678 warrant.addPropertyChangeListener(listener = (PropertyChangeEvent e) -> { 679 synchronized (self) { 680 log.trace("notify waitWarrantBlock of property change"); 681 self.notifyAll(); // should be only one thread waiting, but just in case 682 } 683 }); 684 685 while (warrant.getCurrentBlockName().equals(block) != occupied) { 686 wait(-1); 687 } 688 689 // remove the listener 690 warrant.removePropertyChangeListener(listener); 691 692 } 693 694 private volatile boolean blockChanged = false; 695 private volatile String blockName = null; 696 697 /** 698 * Wait for a warrant to either enter a new block or to stop running. 699 * <p> 700 * This works by registering a listener, which is likely to run in another 701 * thread. That listener then interrupts the automaton's thread, who 702 * confirms the change. 703 * 704 * @param warrant The name of the warrant to watch 705 * 706 * @return The name of the block that was entered or null if the warrant is 707 * no longer running. 708 */ 709 public synchronized String waitWarrantBlockChange(@Nonnull Warrant warrant) { 710 if (!inThread) { 711 log.warn("waitWarrantBlockChange invoked from invalid context"); 712 } 713 if (log.isDebugEnabled()) { 714 log.debug("waitWarrantBlockChange {}", warrant.getDisplayName()); 715 } 716 717 // do a quick check first, just in case 718 if (warrant.getRunMode() != Warrant.MODE_RUN) { 719 log.debug("waitWarrantBlockChange returns immediately"); 720 return null; 721 } 722 // register listeners 723 blockName = null; 724 blockChanged = false; 725 726 PropertyChangeListener listener; 727 warrant.addPropertyChangeListener(listener = (PropertyChangeEvent e) -> { 728 if (e.getPropertyName().equals("blockChange")) { 729 blockChanged = true; 730 blockName = ((OBlock) e.getNewValue()).getDisplayName(); 731 } 732 if (e.getPropertyName().equals("StopWarrant")) { 733 blockName = null; 734 blockChanged = true; 735 } 736 synchronized (self) { 737 log.trace("notify waitWarrantBlockChange of property change"); 738 self.notifyAll(); // should be only one thread waiting, but just in case 739 } 740 }); 741 742 while (!blockChanged) { 743 wait(-1); 744 } 745 746 // remove the listener 747 warrant.removePropertyChangeListener(listener); 748 749 return blockName; 750 } 751 752 /** 753 * Wait for a list of turnouts to all be in a consistent state 754 * <p> 755 * This works by registering a listener, which is likely to run in another 756 * thread. That listener then interrupts the automaton's thread, who 757 * confirms the change. 758 * 759 * @param mTurnouts list of turnouts to watch 760 */ 761 public synchronized void waitTurnoutConsistent(@Nonnull Turnout[] mTurnouts) { 762 if (!inThread) { 763 log.warn("waitTurnoutConsistent invoked from invalid context"); 764 } 765 if (log.isDebugEnabled()) { 766 log.debug("waitTurnoutConsistent[] starts"); 767 } 768 769 // do a quick check first, just in case 770 if (checkForConsistent(mTurnouts)) { 771 log.debug("returns immediately"); 772 return; 773 } 774 // register listeners 775 int i; 776 PropertyChangeListener[] listeners 777 = new PropertyChangeListener[mTurnouts.length]; 778 for (i = 0; i < mTurnouts.length; i++) { 779 780 mTurnouts[i].addPropertyChangeListener(listeners[i] = (PropertyChangeEvent e) -> { 781 synchronized (self) { 782 log.trace("notify waitTurnoutConsistent[] of property change"); 783 self.notifyAll(); // should be only one thread waiting, but just in case 784 } 785 }); 786 787 } 788 789 while (!checkForConsistent(mTurnouts)) { 790 wait(-1); 791 } 792 793 // remove the listeners 794 for (i = 0; i < mTurnouts.length; i++) { 795 mTurnouts[i].removePropertyChangeListener(listeners[i]); 796 } 797 798 } 799 800 /** 801 * Convenience function to set a bunch of turnouts and wait until they are 802 * all in a consistent state 803 * 804 * @param closed turnouts to set to closed state 805 * @param thrown turnouts to set to thrown state 806 */ 807 public void setTurnouts(@Nonnull Turnout[] closed, @Nonnull Turnout[] thrown) { 808 Turnout[] turnouts = new Turnout[closed.length + thrown.length]; 809 int ti = 0; 810 for (int i = 0; i < closed.length; ++i) { 811 turnouts[ti++] = closed[i]; 812 closed[i].setCommandedState(Turnout.CLOSED); 813 } 814 for (int i = 0; i < thrown.length; ++i) { 815 turnouts[ti++] = thrown[i]; 816 thrown[i].setCommandedState(Turnout.THROWN); 817 } 818 waitTurnoutConsistent(turnouts); 819 } 820 821 /** 822 * Wait, up to a specified time, for one of a list of NamedBeans (sensors, 823 * signal heads and/or turnouts) to change their state. 824 * <p> 825 * Registers a listener on each of the NamedBeans listed. The listener is 826 * likely to run in another thread. Each fired listener then queues a check 827 * to the automaton's thread. 828 * 829 * @param mInputs Array of NamedBeans to watch 830 * @param maxDelay maximum amount of time (milliseconds) to wait before 831 * continuing anyway. -1 means forever 832 */ 833 public void waitChange(@Nonnull NamedBean[] mInputs, int maxDelay) { 834 if (!inThread) { 835 log.warn("waitChange invoked from invalid context"); 836 } 837 838 int i; 839 int[] tempState = waitChangePrecheckStates; 840 // do we need to create it now? 841 boolean recreate = false; 842 if (waitChangePrecheckBeans != null && waitChangePrecheckStates != null) { 843 // Seems precheck intended, see if done right 844 if (waitChangePrecheckBeans.length != mInputs.length) { 845 log.warn("Precheck ignored because of mismatch in size: before {}, now {}", waitChangePrecheckBeans.length, mInputs.length); 846 recreate = true; 847 } 848 if (waitChangePrecheckBeans.length != waitChangePrecheckStates.length) { 849 log.error("Precheck data inconsistent because of mismatch in size: {}, {}", waitChangePrecheckBeans.length, waitChangePrecheckStates.length); 850 recreate = true; 851 } 852 if (!recreate) { // have to check if the beans are the same, but only check if the above tests pass 853 for (i = 0; i < mInputs.length; i++) { 854 if (waitChangePrecheckBeans[i] != mInputs[i]) { 855 log.warn("Precheck ignored because of mismatch in bean {}", i); 856 recreate = true; 857 break; 858 } 859 } 860 } 861 } else { 862 recreate = true; 863 } 864 865 if (recreate) { 866 // here, have to create a new state array 867 log.trace("recreate state array"); 868 tempState = new int[mInputs.length]; 869 for (i = 0; i < mInputs.length; i++) { 870 tempState[i] = mInputs[i].getState(); 871 } 872 } 873 waitChangePrecheckBeans = null; 874 waitChangePrecheckStates = null; 875 final int[] initialState = tempState; // needs to be final for off-thread references 876 877 log.debug("waitChange[] starts for {} listeners", mInputs.length); 878 waitChangeQueue.clear(); 879 880 // register listeners 881 PropertyChangeListener[] listeners = new PropertyChangeListener[mInputs.length]; 882 for (i = 0; i < mInputs.length; i++) { 883 mInputs[i].addPropertyChangeListener(listeners[i] = (PropertyChangeEvent e) -> { 884 if (!waitChangeQueue.offer(e)) { 885 log.warn("Waiting changes capacity exceeded; not adding {} to queue", e); 886 } 887 }); 888 889 } 890 891 log.trace("waitChange[] listeners registered"); 892 893 // queue a check for whether there was a change while registering 894 jmri.util.ThreadingUtil.runOnLayoutEventually(() -> { 895 log.trace("start separate waitChange check"); 896 for (int j = 0; j < mInputs.length; j++) { 897 if (initialState[j] != mInputs[j].getState()) { 898 log.trace("notify that input {} changed when initial on-layout check was finally done", j); 899 PropertyChangeEvent e = new PropertyChangeEvent(mInputs[j], "State", initialState[j], mInputs[j].getState()); 900 if (!waitChangeQueue.offer(e)) { 901 log.warn("Waiting changes capacity exceeded; not adding {} to queue", e); 902 } 903 break; 904 } 905 } 906 log.trace("end separate waitChange check"); 907 }); 908 909 // wait for notify from a listener 910 startWait(); 911 912 PropertyChangeEvent prompt; 913 try { 914 if (maxDelay < 0) { 915 prompt = waitChangeQueue.take(); 916 } else { 917 prompt = waitChangeQueue.poll(maxDelay, TimeUnit.MILLISECONDS); 918 } 919 if (prompt != null) { 920 log.trace("waitChange continues from {}", prompt.getSource()); 921 } else { 922 log.trace("waitChange continues"); 923 } 924 } catch (InterruptedException e) { 925 Thread.currentThread().interrupt(); // retain if needed later 926 log.warn("AbstractAutomaton {} waitChange interrupted", getName()); 927 } 928 929 // remove the listeners 930 for (i = 0; i < mInputs.length; i++) { 931 mInputs[i].removePropertyChangeListener(listeners[i]); 932 } 933 log.trace("waitChange[] listeners removed"); 934 endWait(); 935 } 936 937 NamedBean[] waitChangePrecheckBeans = null; 938 int[] waitChangePrecheckStates = null; 939 BlockingQueue<PropertyChangeEvent> waitChangeQueue = new LinkedBlockingQueue<PropertyChangeEvent>(); 940 941 /** 942 * Wait forever for one of a list of NamedBeans (sensors, signal heads 943 * and/or turnouts) to change, or for a specific time to pass. 944 * 945 * @param mInputs Array of NamedBeans to watch 946 */ 947 public void waitChangePrecheck(NamedBean[] mInputs) { 948 waitChangePrecheckBeans = new NamedBean[mInputs.length]; 949 waitChangePrecheckStates = new int[mInputs.length]; 950 for (int i = 0; i < mInputs.length; i++) { 951 waitChangePrecheckBeans[i] = mInputs[i]; 952 waitChangePrecheckStates[i] = mInputs[i].getState(); 953 } 954 } 955 956 /** 957 * Wait forever for one of a list of NamedBeans (sensors, signal heads 958 * and/or turnouts) to change, or for a specific time to pass. 959 * 960 * @param mInputs Array of NamedBeans to watch 961 */ 962 public void waitChange(NamedBean[] mInputs) { 963 waitChange(mInputs, -1); 964 } 965 966 /** 967 * Wait for one of an array of sensors to change. 968 * <p> 969 * This is an older method, now superceded by waitChange, which can wait for 970 * any NamedBean. 971 * 972 * @param mSensors Array of sensors to watch 973 */ 974 public void waitSensorChange(Sensor[] mSensors) { 975 waitChange(mSensors); 976 } 977 978 /** 979 * Check an array of sensors to see if any are in a specific state 980 * 981 * @param mSensors Array to check 982 * @return true if any are ACTIVE 983 */ 984 private boolean checkForState(Sensor[] mSensors, int state) { 985 for (Sensor mSensor : mSensors) { 986 if (mSensor.getKnownState() == state) { 987 return true; 988 } 989 } 990 return false; 991 } 992 993 private boolean checkForConsistent(Turnout[] mTurnouts) { 994 for (int i = 0; i < mTurnouts.length; ++i) { 995 if (!mTurnouts[i].isConsistentState()) { 996 return false; 997 } 998 } 999 return true; 1000 } 1001 1002 private DccThrottle throttle; 1003 private boolean failedThrottleRequest = false; 1004 1005 /** 1006 * Obtains a DCC throttle, including waiting for the command station 1007 * response. 1008 * 1009 * @param address Numeric address value 1010 * @param longAddress true if this is a long address, false for a short 1011 * address 1012 * @param waitSecs number of seconds to wait for throttle to acquire 1013 * before returning null 1014 * @return A usable throttle, or null if error 1015 */ 1016 public DccThrottle getThrottle(int address, boolean longAddress, int waitSecs) { 1017 log.debug("requesting DccThrottle for addr {}", address); 1018 if (!inThread) { 1019 log.warn("getThrottle invoked from invalid context"); 1020 } 1021 throttle = null; 1022 ThrottleListener throttleListener = new ThrottleListener() { 1023 @Override 1024 public void notifyThrottleFound(DccThrottle t) { 1025 throttle = t; 1026 synchronized (self) { 1027 self.notifyAll(); // should be only one thread waiting, but just in case 1028 } 1029 } 1030 1031 @Override 1032 public void notifyFailedThrottleRequest(jmri.LocoAddress address, String reason) { 1033 log.error("Throttle request failed for {} because {}", address, reason); 1034 failedThrottleRequest = true; 1035 synchronized (self) { 1036 self.notifyAll(); // should be only one thread waiting, but just in case 1037 } 1038 } 1039 1040 /** 1041 * No steal or share decisions made locally 1042 */ 1043 @Override 1044 public void notifyDecisionRequired(jmri.LocoAddress address, DecisionType question) { 1045 } 1046 }; 1047 boolean ok = InstanceManager.getDefault(ThrottleManager.class).requestThrottle( 1048 new jmri.DccLocoAddress(address, longAddress), throttleListener, false); 1049 1050 // check if reply is coming 1051 if (!ok) { 1052 log.info("Throttle for loco {} not available",address); 1053 InstanceManager.getDefault(ThrottleManager.class).cancelThrottleRequest( 1054 new jmri.DccLocoAddress(address, longAddress), throttleListener); //kill the pending request 1055 return null; 1056 } 1057 1058 // now wait for reply from identified throttle 1059 int waited = 0; 1060 while (throttle == null && failedThrottleRequest == false && waited <= waitSecs) { 1061 log.debug("waiting for throttle"); 1062 wait(1000); // 1 seconds 1063 waited++; 1064 if (throttle == null) { 1065 log.warn("Still waiting for throttle {}!", address); 1066 } 1067 } 1068 if (throttle == null) { 1069 log.debug("canceling request for Throttle {}", address); 1070 InstanceManager.getDefault(ThrottleManager.class).cancelThrottleRequest( 1071 new jmri.DccLocoAddress(address, longAddress), throttleListener); //kill the pending request 1072 } 1073 return throttle; 1074 } 1075 1076 public DccThrottle getThrottle(int address, boolean longAddress) { 1077 return getThrottle(address, longAddress, 30); //default to 30 seconds wait 1078 } 1079 1080 /** 1081 * Obtains a DCC throttle, including waiting for the command station 1082 * response. 1083 * 1084 * @param re specifies the desired locomotive 1085 * @param waitSecs number of seconds to wait for throttle to acquire before 1086 * returning null 1087 * @return A usable throttle, or null if error 1088 */ 1089 public DccThrottle getThrottle(BasicRosterEntry re, int waitSecs) { 1090 log.debug("requesting DccThrottle for rosterEntry {}", re.getId()); 1091 if (!inThread) { 1092 log.warn("getThrottle invoked from invalid context"); 1093 } 1094 throttle = null; 1095 ThrottleListener throttleListener = new ThrottleListener() { 1096 @Override 1097 public void notifyThrottleFound(DccThrottle t) { 1098 throttle = t; 1099 synchronized (self) { 1100 self.notifyAll(); // should be only one thread waiting, but just in case 1101 } 1102 } 1103 1104 @Override 1105 public void notifyFailedThrottleRequest(jmri.LocoAddress address, String reason) { 1106 log.error("Throttle request failed for {} because {}", address, reason); 1107 failedThrottleRequest = true; 1108 synchronized (self) { 1109 self.notifyAll(); // should be only one thread waiting, but just in case 1110 } 1111 } 1112 1113 /** 1114 * No steal or share decisions made locally 1115 * {@inheritDoc} 1116 */ 1117 @Override 1118 public void notifyDecisionRequired(jmri.LocoAddress address, DecisionType question) { 1119 } 1120 }; 1121 boolean ok = InstanceManager.getDefault(ThrottleManager.class) 1122 .requestThrottle(re, throttleListener, false); 1123 1124 // check if reply is coming 1125 if (!ok) { 1126 log.info("Throttle for loco {} not available", re.getId()); 1127 InstanceManager.getDefault(ThrottleManager.class).cancelThrottleRequest( 1128 re.getDccLocoAddress(), throttleListener); //kill the pending request 1129 return null; 1130 } 1131 1132 // now wait for reply from identified throttle 1133 int waited = 0; 1134 while (throttle == null && failedThrottleRequest == false && waited <= waitSecs) { 1135 log.debug("waiting for throttle"); 1136 wait(1000); // 1 seconds 1137 waited++; 1138 if (throttle == null) { 1139 log.warn("Still waiting for throttle {}!", re.getId()); 1140 } 1141 } 1142 if (throttle == null) { 1143 log.debug("canceling request for Throttle {}", re.getId()); 1144 InstanceManager.getDefault(ThrottleManager.class).cancelThrottleRequest( 1145 re.getDccLocoAddress(), throttleListener); //kill the pending request 1146 } 1147 return throttle; 1148 } 1149 1150 public DccThrottle getThrottle(BasicRosterEntry re) { 1151 return getThrottle(re, 30); //default to 30 seconds 1152 } 1153 1154 /** 1155 * Write a CV on the service track, including waiting for completion. 1156 * 1157 * @param CV Number 1 through 512 1158 * @param value Value 0-255 to be written 1159 * @return true if completed OK 1160 */ 1161 public boolean writeServiceModeCV(String CV, int value) { 1162 // get service mode programmer 1163 Programmer programmer = InstanceManager.getDefault(jmri.GlobalProgrammerManager.class) 1164 .getGlobalProgrammer(); 1165 1166 if (programmer == null) { 1167 log.error("No programmer available as JMRI is currently configured"); 1168 return false; 1169 } 1170 1171 // do the write, response will wake the thread 1172 try { 1173 programmer.writeCV(CV, value, (int value1, int status) -> { 1174 synchronized (self) { 1175 self.notifyAll(); // should be only one thread waiting, but just in case 1176 } 1177 }); 1178 } catch (ProgrammerException e) { 1179 log.warn("Exception during writeServiceModeCV", e); 1180 return false; 1181 } 1182 // wait for the result 1183 wait(-1); 1184 1185 return true; 1186 } 1187 1188 private volatile int cvReturnValue; 1189 1190 /** 1191 * Read a CV on the service track, including waiting for completion. 1192 * 1193 * @param CV Number 1 through 512 1194 * @return -1 if error, else value 1195 */ 1196 public int readServiceModeCV(String CV) { 1197 // get service mode programmer 1198 Programmer programmer = InstanceManager.getDefault(jmri.GlobalProgrammerManager.class) 1199 .getGlobalProgrammer(); 1200 1201 if (programmer == null) { 1202 log.error("No programmer available as JMRI is currently configured"); 1203 return -1; 1204 } 1205 1206 // do the read, response will wake the thread 1207 cvReturnValue = -1; 1208 try { 1209 programmer.readCV(CV, (int value, int status) -> { 1210 cvReturnValue = value; 1211 synchronized (self) { 1212 self.notifyAll(); // should be only one thread waiting, but just in case 1213 } 1214 }); 1215 } catch (ProgrammerException e) { 1216 log.warn("Exception during writeServiceModeCV", e); 1217 return -1; 1218 } 1219 // wait for the result 1220 wait(-1); 1221 return cvReturnValue; 1222 } 1223 1224 /** 1225 * Write a CV in ops mode, including waiting for completion. 1226 * 1227 * @param CV Number 1 through 512 1228 * @param value 0-255 value to be written 1229 * @param loco Locomotive decoder address 1230 * @param longAddress true is the locomotive is using a long address 1231 * @return true if completed OK 1232 */ 1233 public boolean writeOpsModeCV(String CV, int value, boolean longAddress, int loco) { 1234 // get service mode programmer 1235 Programmer programmer = InstanceManager.getDefault(jmri.AddressedProgrammerManager.class) 1236 .getAddressedProgrammer(longAddress, loco); 1237 1238 if (programmer == null) { 1239 log.error("No programmer available as JMRI is currently configured"); 1240 return false; 1241 } 1242 1243 // do the write, response will wake the thread 1244 try { 1245 programmer.writeCV(CV, value, (int value1, int status) -> { 1246 synchronized (self) { 1247 self.notifyAll(); // should be only one thread waiting, but just in case 1248 } 1249 }); 1250 } catch (ProgrammerException e) { 1251 log.warn("Exception during writeServiceModeCV", e); 1252 return false; 1253 } 1254 // wait for the result 1255 wait(-1); 1256 1257 return true; 1258 } 1259 1260 JFrame messageFrame = null; 1261 String message = null; 1262 1263 /** 1264 * Internal class to show a Frame 1265 */ 1266 public class MsgFrame implements Runnable { 1267 1268 String mMessage; 1269 boolean mPause; 1270 boolean mShow; 1271 JFrame mFrame = null; 1272 JButton mButton; 1273 JTextArea mArea; 1274 1275 public void hide() { 1276 mShow = false; 1277 // invoke the operation 1278 javax.swing.SwingUtilities.invokeLater(this); 1279 } 1280 1281 /** 1282 * Show a message in the message frame, and optionally wait for the user 1283 * to acknowledge. 1284 * 1285 * @param pMessage the message to show 1286 * @param pPause true if this automaton should wait for user 1287 * acknowledgment; false otherwise 1288 */ 1289 public void show(String pMessage, boolean pPause) { 1290 mMessage = pMessage; 1291 mPause = pPause; 1292 mShow = true; 1293 1294 // invoke the operation 1295 javax.swing.SwingUtilities.invokeLater(this); 1296 // wait to proceed? 1297 if (mPause) { 1298 synchronized (self) { 1299 new jmri.util.WaitHandler(this); 1300 } 1301 } 1302 } 1303 1304 @Override 1305 public void run() { 1306 // create the frame if it doesn't exist 1307 if (mFrame == null) { 1308 mFrame = new JFrame(""); 1309 mArea = new JTextArea(); 1310 mArea.setEditable(false); 1311 mArea.setLineWrap(false); 1312 mArea.setWrapStyleWord(true); 1313 mButton = new JButton("Continue"); 1314 mFrame.getContentPane().setLayout(new BorderLayout()); 1315 mFrame.getContentPane().add(mArea, BorderLayout.CENTER); 1316 mFrame.getContentPane().add(mButton, BorderLayout.SOUTH); 1317 mButton.addActionListener((java.awt.event.ActionEvent e) -> { 1318 synchronized (self) { 1319 self.notifyAll(); // should be only one thread waiting, but just in case 1320 } 1321 mFrame.setVisible(false); 1322 }); 1323 mFrame.pack(); 1324 } 1325 if (mShow) { 1326 // update message, show button if paused 1327 mArea.setText(mMessage); 1328 if (mPause) { 1329 mButton.setVisible(true); 1330 } else { 1331 mButton.setVisible(false); 1332 } 1333 // do optional formatting 1334 format(); 1335 // center the frame 1336 mFrame.pack(); 1337 Dimension screen = mFrame.getContentPane().getToolkit().getScreenSize(); 1338 Dimension size = mFrame.getSize(); 1339 mFrame.setLocation((screen.width - size.width) / 2, (screen.height - size.height) / 2); 1340 // and show it to the user 1341 mFrame.setVisible(true); 1342 } else { 1343 mFrame.setVisible(false); 1344 } 1345 } 1346 1347 /** 1348 * Abstract method to handle formatting of the text on a show 1349 */ 1350 protected void format() { 1351 } 1352 } 1353 1354 JFrame debugWaitFrame = null; 1355 1356 /** 1357 * Wait for the user to OK moving forward. This is complicated by not 1358 * running in the GUI thread, and by not wanting to use a modal dialog. 1359 */ 1360 private void debuggingWait() { 1361 // post an event to the GUI pane 1362 Runnable r = () -> { 1363 // create a prompting frame 1364 if (debugWaitFrame == null) { 1365 debugWaitFrame = new JFrame("Automaton paused"); 1366 JButton b = new JButton("Continue"); 1367 debugWaitFrame.getContentPane().add(b); 1368 b.addActionListener((java.awt.event.ActionEvent e) -> { 1369 synchronized (self) { 1370 self.notifyAll(); // should be only one thread waiting, but just in case 1371 } 1372 debugWaitFrame.setVisible(false); 1373 }); 1374 debugWaitFrame.pack(); 1375 } 1376 debugWaitFrame.setVisible(true); 1377 }; 1378 javax.swing.SwingUtilities.invokeLater(r); 1379 // wait to proceed 1380 try { 1381 super.wait(); 1382 } catch (InterruptedException e) { 1383 Thread.currentThread().interrupt(); // retain if needed later 1384 log.warn("Interrupted during debugging wait, not expected"); 1385 } 1386 } 1387 // initialize logging 1388 private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(AbstractAutomaton.class); 1389}