001package jmri.implementation; 002 003import java.beans.*; 004import java.time.LocalDateTime; 005import java.util.*; 006 007import javax.annotation.CheckForNull; 008import javax.annotation.Nonnull; 009 010import jmri.*; 011 012/** 013 * Abstract base for the Turnout interface. 014 * <p> 015 * Implements basic feedback modes: 016 * <ul> 017 * <li>NONE feedback, where the KnownState and CommandedState track each other. 018 * <li>ONESENSOR feedback where the state of a single sensor specifies THROWN vs 019 * CLOSED 020 * <li>TWOSENSOR feedback, where one sensor specifies THROWN and another CLOSED. 021 * </ul> 022 * If you want to implement some other feedback, override and modify 023 * setCommandedState() here. 024 * <p> 025 * Implements the parameter binding support. 026 * <p> 027 * Note that we consider it an error for there to be more than one object that 028 * corresponds to a particular physical turnout on the layout. 029 * 030 * @author Bob Jacobsen Copyright (C) 2001, 2009 031 */ 032public abstract class AbstractTurnout extends AbstractNamedBean implements 033 Turnout, PropertyChangeListener { 034 035 private Turnout leadingTurnout = null; 036 private boolean followingCommandedState = true; 037 038 protected AbstractTurnout(String systemName) { 039 super(systemName); 040 } 041 042 protected AbstractTurnout(String systemName, String userName) { 043 super(systemName, userName); 044 } 045 046 /** {@inheritDoc} */ 047 @Override 048 @Nonnull 049 public String getBeanType() { 050 return Bundle.getMessage("BeanNameTurnout"); 051 } 052 053 private final String closedText = InstanceManager.turnoutManagerInstance().getClosedText(); 054 private final String thrownText = InstanceManager.turnoutManagerInstance().getThrownText(); 055 056 /** 057 * Handle a request to change state, typically by sending a message to the 058 * layout in some child class. Public version (used by TurnoutOperator) 059 * sends the current commanded state without changing it. 060 * Implementing classes will typically check the value of s and send a system specific sendMessage command. 061 * 062 * @param s new state value 063 */ 064 abstract protected void forwardCommandChangeToLayout(int s); 065 066 protected void forwardCommandChangeToLayout() { 067 forwardCommandChangeToLayout(_commandedState); 068 } 069 070 /** 071 * Preprocess a Turnout state change request for {@link #forwardCommandChangeToLayout(int)} 072 * Public access to allow use in tests. 073 * 074 * @param newState the Turnout state command value passed 075 * @return true if a Turnout.CLOSED was requested and Turnout is not set to _inverted 076 * @throws IllegalArgumentException when needed 077 */ 078 public boolean stateChangeCheck(int newState) throws IllegalArgumentException { 079 // sort out states 080 if ((newState & Turnout.CLOSED) != 0) { 081 if (statesOk(newState)) { 082 // request a CLOSED command (or THROWN if inverted) 083 return (!_inverted); 084 } else { 085 throw new IllegalArgumentException("Can't set state for Turnout " + newState); 086 } 087 } 088 // request a THROWN command (or CLOSED if inverted) 089 return (_inverted); 090 } 091 092 /** 093 * Look for the case in which the state is neither Closed nor Thrown, which we can't handle. 094 * Separate method to allow it to be used in {@link #stateChangeCheck} and Xpa/MqttTurnout. 095 * 096 * @param state the Turnout state passed 097 * @return false if s = Turnout.THROWN, which is what we want 098 */ 099 protected boolean statesOk(int state) { 100 if ((state & Turnout.THROWN) != 0) { 101 // this is the disaster case! 102 log.error("Cannot command both CLOSED and THROWN"); 103 return false; 104 } 105 return true; 106 } 107 108 /** 109 * Set a new Commanded state, if need be notifying the listeners, but do 110 * NOT send the command downstream. 111 * <p> 112 * This is used when a new commanded state 113 * is noticed from another command. 114 * 115 * @param s new state 116 */ 117 protected void newCommandedState(int s) { 118 if (_commandedState != s) { 119 int oldState = _commandedState; 120 _commandedState = s; 121 firePropertyChange(PROPERTY_COMMANDED_STATE, oldState, _commandedState); 122 } 123 } 124 125 /** {@inheritDoc} */ 126 @Override 127 public int getKnownState() { 128 return _knownState; 129 } 130 131 /** 132 * Public access to changing turnout state. Sets the commanded state and, if 133 * appropriate, starts a TurnoutOperator to do its thing. If there is no 134 * TurnoutOperator (not required or nothing suitable) then just tell the 135 * layout and hope for the best. 136 * 137 * @param s commanded state to set 138 */ 139 @Override 140 public void setCommandedState(int s) { 141 log.debug("set commanded state for turnout {} to {}", getDisplayName(DisplayOptions.USERNAME_SYSTEMNAME), 142 (s == Turnout.CLOSED ? closedText : thrownText)); 143 newCommandedState(s); 144 myOperator = getTurnoutOperator(); // MUST set myOperator before starting the thread 145 if (myOperator == null) { 146 log.debug("myOperator NULL"); 147 forwardCommandChangeToLayout(s); 148 // optionally handle feedback 149 if (_activeFeedbackType == DIRECT) { 150 newKnownState(s); 151 } else if (_activeFeedbackType == DELAYED) { 152 newKnownStateDelayed(s); 153 } 154 } else { 155 log.debug("myOperator NOT NULL"); 156 myOperator.start(); 157 } 158 } 159 160 /** 161 * Duration in Milliseconds of delay for DELAYED feedback mode. 162 * <p> 163 * Defined as "public non-final" so it can be changed in e.g. 164 * the jython/SetDefaultDelayedTurnoutDelay script. 165 */ 166 public static int DELAYED_FEEDBACK_INTERVAL = 4000; 167 168 /** 169 * Counts the DELAYED feedback timers started, so that a timer from an 170 * earlier command does not overwrite the state of a later one. 171 */ 172 private final java.util.concurrent.atomic.AtomicInteger delayedFeedbackCount 173 = new java.util.concurrent.atomic.AtomicInteger(); 174 175 /** 176 * For DELAYED feedback: set the known state to INCONSISTENT now and to 177 * the given state after {@link #DELAYED_FEEDBACK_INTERVAL}, unless 178 * another DELAYED feedback change has been started in the meantime. 179 * 180 * @param s the state the turnout will be in after the delay 181 */ 182 protected void newKnownStateDelayed(int s) { 183 int count = delayedFeedbackCount.incrementAndGet(); 184 newKnownState(INCONSISTENT); 185 jmri.util.ThreadingUtil.runOnLayoutDelayed( () -> { 186 if (count == delayedFeedbackCount.get()) { 187 newKnownState(s); 188 } 189 }, DELAYED_FEEDBACK_INTERVAL ); 190 } 191 192 /** 193 * No longer used: delayed commands from 194 * {@link #setCommandedStateAtInterval(int)} run on the layout thread. 195 */ 196 @Deprecated(since="5.17.5", forRemoval=true) 197 protected Thread thr; 198 /** 199 * No longer used: delayed commands from 200 * {@link #setCommandedStateAtInterval(int)} run on the layout thread. 201 */ 202 @Deprecated(since="5.17.5", forRemoval=true) 203 protected Runnable r; 204 205 /** {@inheritDoc} 206 * Used in {@link jmri.implementation.DefaultRoute#setRoute()} and 207 * {@link jmri.implementation.MatrixSignalMast#updateOutputs(char[])}. 208 */ 209 @Override 210 public void setCommandedStateAtInterval(int s) { 211 // each call gets its own end time, later calls get later times 212 LocalDateTime nextWait = InstanceManager.turnoutManagerInstance().outputIntervalEnds(); 213 // nextWait time is calculated using actual turnoutInterval in TurnoutManager 214 if (nextWait.isAfter(LocalDateTime.now())) { // don't wait if nextWait =< now() 215 log.debug("Turnout now() = {}, waitUntil = {}", LocalDateTime.now(), nextWait); 216 // insert wait before sending next output command to the layout; 217 // the delayed commands run on the layout thread at their end 218 // times, so in the order of those times 219 Date runAt = Date.from(nextWait.atZone(java.time.ZoneId.systemDefault()).toInstant()); 220 jmri.util.TimerUtil.scheduleOnLayoutThread(new TimerTask() { 221 @Override 222 public void run() { 223 log.debug("back from wait, forward on {}", LocalDateTime.now()); 224 setCommandedState(s); 225 } 226 }, runAt); 227 } else { 228 log.debug("nextWait has passed"); 229 setCommandedState(s); 230 } 231 } 232 233 /** {@inheritDoc} */ 234 @Override 235 public int getCommandedState() { 236 return _commandedState; 237 } 238 239 /** 240 * Add a newKnownState() for use by implementations. 241 * <p> 242 * Use this to update internal information when a state change is detected 243 * <em>outside</em> the Turnout object, e.g. via feedback from sensors on 244 * the layout. 245 * <p> 246 * If the layout status of the Turnout is observed to change to THROWN or 247 * CLOSED, this also sets the commanded state, because it's assumed that 248 * somebody somewhere commanded that move. If it's observed to change to 249 * UNKNOWN or INCONSISTENT, that's perhaps either an error or a move in 250 * progress, and no change is made to the commanded state. 251 * <p> 252 * This implementation sends a command to the layout for the new state if 253 * going to THROWN or CLOSED, because there may be others listening to 254 * network state. 255 * <p> 256 * This method is not intended for general use, e.g. for users to set the 257 * KnownState, so it doesn't appear in the Turnout interface. 258 * <p> 259 * On change, fires Property Change "KnownState". 260 * @param s New state value 261 */ 262 public void newKnownState(int s) { 263 if (_knownState != s) { 264 int oldState = _knownState; 265 _knownState = s; 266 firePropertyChange(PROPERTY_KNOWN_STATE, oldState, _knownState); 267 } 268 _knownState = s; 269 // if known state has moved to Thrown or Closed, 270 // set the commanded state to match 271 if ((_knownState == THROWN && _commandedState != THROWN) 272 || (_knownState == CLOSED && _commandedState != CLOSED)) { 273 newCommandedState(_knownState); 274 } 275 } 276 277 /** 278 * Show whether state is one you can safely run trains over. 279 * 280 * @return true if state is a valid one and the known state is the same as 281 * commanded. 282 */ 283 @Override 284 public boolean isConsistentState() { 285 return _commandedState == _knownState 286 && (_commandedState == CLOSED || _commandedState == THROWN); 287 } 288 289 /** 290 * The name pretty much says it. 291 * <p> 292 * Triggers all listeners, etc. For use by the TurnoutOperator classes. 293 */ 294 void setKnownStateToCommanded() { 295 newKnownState(_commandedState); 296 } 297 298 /** 299 * Implement a shorter name for setCommandedState. 300 * <p> 301 * This generally shouldn't be used by Java code; use setCommandedState 302 * instead. The is provided to make Jython script access easier to read. 303 * <p> 304 * Note that getState() and setState(int) are not symmetric: getState is the 305 * known state, and set state modifies the commanded state. 306 * @param s new state 307 */ 308 @Override 309 public void setState(int s) { 310 setCommandedState(s); 311 } 312 313 /** 314 * Implement a shorter name for getKnownState. 315 * <p> 316 * This generally shouldn't be used by Java code; use getKnownState instead. 317 * The is provided to make Jython script access easier to read. 318 * <p> 319 * Note that getState() and setState(int) are not symmetric: getState is the 320 * known state, and set state modifies the commanded state. 321 * @return current state 322 */ 323 @Override 324 public int getState() { 325 return getKnownState(); 326 } 327 328 /** {@inheritDoc} */ 329 @Override 330 @Nonnull 331 public String describeState(int state) { 332 switch (state) { 333 case THROWN: return thrownText; 334 case CLOSED: return closedText; 335 default: return super.describeState(state); 336 } 337 } 338 339 protected String[] _validFeedbackNames = {"DIRECT", "ONESENSOR", 340 "TWOSENSOR", "DELAYED"}; 341 342 protected int[] _validFeedbackModes = {DIRECT, ONESENSOR, TWOSENSOR, DELAYED}; 343 344 protected int _validFeedbackTypes = DIRECT | ONESENSOR | TWOSENSOR | DELAYED; 345 346 protected int _activeFeedbackType = DIRECT; 347 348 private int _knownState = UNKNOWN; 349 350 private int _commandedState = UNKNOWN; 351 352 private int _numberControlBits = 1; 353 354 /** Number of bits to control a turnout - defaults to one */ 355 private int _controlType = 0; 356 357 /** Type of turnout control - defaults to 0 for /'steady state/' */ 358 @Override 359 public int getNumberControlBits() { 360 return _numberControlBits; 361 } 362 363 /** {@inheritDoc} */ 364 @Override 365 public void setNumberControlBits(int num) { 366 _numberControlBits = num; 367 } 368 369 /** {@inheritDoc} */ 370 @Override 371 public int getControlType() { 372 return _controlType; 373 } 374 375 /** {@inheritDoc} */ 376 @Override 377 public void setControlType(int num) { 378 _controlType = num; 379 } 380 381 /** {@inheritDoc} */ 382 @Override 383 public Set<Integer> getValidFeedbackModes() { 384 Set<Integer> modes = new HashSet<>(); 385 Arrays.stream(_validFeedbackModes).forEach(modes::add); 386 return modes; 387 } 388 389 /** {@inheritDoc} */ 390 @Override 391 public int getValidFeedbackTypes() { 392 return _validFeedbackTypes; 393 } 394 395 /** {@inheritDoc} */ 396 @Override 397 @Nonnull 398 public String[] getValidFeedbackNames() { 399 return Arrays.copyOf(_validFeedbackNames, _validFeedbackNames.length); 400 } 401 402 /** {@inheritDoc} */ 403 @Override 404 public void setFeedbackMode(@Nonnull String mode) throws IllegalArgumentException { 405 for (int i = 0; i < _validFeedbackNames.length; i++) { 406 if (mode.equals(_validFeedbackNames[i])) { 407 setFeedbackMode(_validFeedbackModes[i]); 408 setInitialKnownStateFromFeedback(); 409 return; 410 } 411 } 412 throw new IllegalArgumentException("Unexpected mode: " + mode); 413 } 414 415 /** 416 * On change, fires Property Change "feedbackchange". 417 * {@inheritDoc} 418 */ 419 @Override 420 public void setFeedbackMode(int mode) throws IllegalArgumentException { 421 // check for error - following removed the low bit from mode 422 int test = mode & (mode - 1); 423 if (test != 0) { 424 throw new IllegalArgumentException("More than one bit set: " + mode); 425 } 426 // set the value 427 int oldMode = _activeFeedbackType; 428 _activeFeedbackType = mode; 429 // unlock turnout if feedback is changed 430 setLocked(CABLOCKOUT, false); 431 if (oldMode != _activeFeedbackType) { 432 // a pending DELAYED feedback timer must not set the state any more 433 delayedFeedbackCount.incrementAndGet(); 434 firePropertyChange(PROPERTY_FEEDBACK_MODE, oldMode, 435 _activeFeedbackType); 436 } 437 } 438 439 /** {@inheritDoc} */ 440 @Override 441 public int getFeedbackMode() { 442 return _activeFeedbackType; 443 } 444 445 /** {@inheritDoc} */ 446 @Override 447 @Nonnull 448 public String getFeedbackModeName() { 449 for (int i = 0; i < _validFeedbackNames.length; i++) { 450 if (_activeFeedbackType == _validFeedbackModes[i]) { 451 return _validFeedbackNames[i]; 452 } 453 } 454 throw new IllegalArgumentException("Unexpected internal mode: " 455 + _activeFeedbackType); 456 } 457 458 /** {@inheritDoc} */ 459 @Override 460 public void requestUpdateFromLayout() { 461 if (_activeFeedbackType == ONESENSOR || _activeFeedbackType == TWOSENSOR) { 462 Sensor s1 = getFirstSensor(); 463 if (s1 != null) s1.requestUpdateFromLayout(); 464 } 465 if (_activeFeedbackType == TWOSENSOR) { 466 Sensor s2 = getSecondSensor(); 467 if (s2 != null) s2.requestUpdateFromLayout(); 468 } 469 } 470 471 /** 472 * On change, fires Property Change "inverted". 473 * {@inheritDoc} 474 */ 475 @Override 476 public void setInverted(boolean inverted) { 477 boolean oldInverted = _inverted; 478 _inverted = inverted; 479 if (oldInverted != _inverted) { 480 int state = _knownState; 481 if (state == THROWN) { 482 newKnownState(CLOSED); 483 } else if (state == CLOSED) { 484 newKnownState(THROWN); 485 } 486 firePropertyChange(PROPERTY_INVERTED, oldInverted, _inverted); 487 } 488 } 489 490 /** 491 * Get the turnout inverted state. If true, commands sent to the layout are 492 * reversed. Thrown becomes Closed, and Closed becomes Thrown. 493 * <p> 494 * Used in polling loops in system-specific code, so made final to allow 495 * optimization. 496 * 497 * @return inverted status 498 */ 499 @Override 500 public final boolean getInverted() { 501 return _inverted; 502 } 503 504 protected boolean _inverted = false; 505 506 /** 507 * Determine if the turnouts can be inverted. If true, inverted turnouts 508 * are supported. 509 * @return invert supported 510 */ 511 @Override 512 public boolean canInvert() { 513 return false; 514 } 515 516 /** 517 * Turnouts that are locked should only respond to JMRI commands to change 518 * state. 519 * We simulate a locked turnout by monitoring the known state (turnout 520 * feedback is required) and if we detect that the known state has 521 * changed, 522 * negate it by forcing the turnout to return to the commanded 523 * state. 524 * Turnouts that have local buttons can also be locked if their 525 * decoder supports it. 526 * On change, fires Property Change "locked". 527 * 528 * @param turnoutLockout lockout state to monitor. Possible values 529 * {@link #CABLOCKOUT}, {@link #PUSHBUTTONLOCKOUT}. 530 * Can be combined to monitor both states. 531 * @param locked true if turnout to be locked 532 */ 533 @Override 534 public void setLocked(int turnoutLockout, boolean locked) { 535 boolean firechange = false; 536 if ((turnoutLockout & CABLOCKOUT) != 0 && _cabLockout != locked) { 537 firechange = true; 538 if (canLock(CABLOCKOUT)) { 539 _cabLockout = locked; 540 } else { 541 _cabLockout = false; 542 } 543 } 544 if ((turnoutLockout & PUSHBUTTONLOCKOUT) != 0 545 && _pushButtonLockout != locked) { 546 firechange = true; 547 if (canLock(PUSHBUTTONLOCKOUT)) { 548 _pushButtonLockout = locked; 549 // now change pushbutton lockout state on layout 550 turnoutPushbuttonLockout(); 551 } else { 552 _pushButtonLockout = false; 553 } 554 } 555 if (firechange) { 556 firePropertyChange(PROPERTY_LOCKED, !locked, locked); 557 } 558 } 559 560 /** 561 * Determine if turnout is locked. There 562 * are two types of locks: cab lockout, and pushbutton lockout. 563 * 564 * @param turnoutLockout turnout to check 565 * @return locked state, true if turnout is locked 566 */ 567 @Override 568 public boolean getLocked(int turnoutLockout) { 569 switch (turnoutLockout) { 570 case CABLOCKOUT: 571 return _cabLockout; 572 case PUSHBUTTONLOCKOUT: 573 return _pushButtonLockout; 574 case CABLOCKOUT + PUSHBUTTONLOCKOUT: 575 return _cabLockout || _pushButtonLockout; 576 default: 577 return false; 578 } 579 } 580 581 protected boolean _cabLockout = false; 582 583 protected boolean _pushButtonLockout = false; 584 585 protected boolean _enableCabLockout = false; 586 587 protected boolean _enablePushButtonLockout = false; 588 589 /** 590 * This implementation by itself doesn't provide locking support. 591 * Override this in subclasses that do. 592 * 593 * @return One of 0 for none 594 */ 595 @Override 596 public int getPossibleLockModes() { return 0; } 597 598 /** 599 * This implementation by itself doesn't provide locking support. 600 * Override this in subclasses that do. 601 * 602 * @return false for not supported 603 */ 604 @Override 605 public boolean canLock(int turnoutLockout) { 606 return false; 607 } 608 609 /** {@inheritDoc} 610 * Not implemented in AbstractTurnout. 611 */ 612 @Override 613 public void enableLockOperation(int turnoutLockout, boolean enabled) { 614 } 615 616 /** 617 * When true, report to console anytime a cab attempts to change the state 618 * of a turnout on the layout. 619 * When a turnout is cab locked, only JMRI is 620 * allowed to change the state of a turnout. 621 * On setting changed, fires Property Change "reportlocked". 622 * 623 * @param reportLocked report locked state 624 */ 625 @Override 626 public void setReportLocked(boolean reportLocked) { 627 boolean oldReportLocked = _reportLocked; 628 _reportLocked = reportLocked; 629 if (oldReportLocked != _reportLocked) { 630 firePropertyChange(PROPERTY_REPORT_LOCKED, oldReportLocked, 631 _reportLocked); 632 } 633 } 634 635 /** 636 * When true, report to console anytime a cab attempts to change the state 637 * of a turnout on the layout. When a turnout is cab locked, only JMRI is 638 * allowed to change the state of a turnout. 639 * 640 * @return report locked state 641 */ 642 @Override 643 public boolean getReportLocked() { 644 return _reportLocked; 645 } 646 647 protected boolean _reportLocked = true; 648 649 /** 650 * Valid stationary decoder names. 651 */ 652 protected String[] _validDecoderNames = PushbuttonPacket 653 .getValidDecoderNames(); 654 655 /** {@inheritDoc} */ 656 @Override 657 @Nonnull 658 public String[] getValidDecoderNames() { 659 return Arrays.copyOf(_validDecoderNames, _validDecoderNames.length); 660 } 661 662 // set the turnout decoder default to unknown 663 protected String _decoderName = PushbuttonPacket.unknown; 664 665 /** {@inheritDoc} */ 666 @Override 667 public String getDecoderName() { 668 return _decoderName; 669 } 670 671 /** 672 * {@inheritDoc} 673 * On change, fires Property Change "decoderNameChange". 674 */ 675 @Override 676 public void setDecoderName(final String decoderName) { 677 if (!(Objects.equals(_decoderName, decoderName))) { 678 String oldName = _decoderName; 679 _decoderName = decoderName; 680 firePropertyChange(PROPERTY_DECODER_NAME, oldName, decoderName); 681 } 682 } 683 684 abstract protected void turnoutPushbuttonLockout(boolean locked); 685 686 protected void turnoutPushbuttonLockout() { 687 turnoutPushbuttonLockout(_pushButtonLockout); 688 } 689 690 /* 691 * Support for turnout automation (see TurnoutOperation and related classes). 692 */ 693 protected TurnoutOperator myOperator; 694 695 protected TurnoutOperation myTurnoutOperation; 696 697 protected boolean inhibitOperation = true; // do not automate this turnout, even if globally operations are on 698 699 public TurnoutOperator getCurrentOperator() { 700 return myOperator; 701 } 702 703 /** {@inheritDoc} */ 704 @Override 705 public TurnoutOperation getTurnoutOperation() { 706 return myTurnoutOperation; 707 } 708 709 /** 710 * {@inheritDoc} 711 * Fires Property Change "TurnoutOperationState". 712 */ 713 @Override 714 public void setTurnoutOperation(TurnoutOperation toper) { 715 log.debug("setTurnoutOperation Called for turnout {}. Operation type {}", this.getSystemName(), toper); 716 TurnoutOperation oldOp = myTurnoutOperation; 717 if (myTurnoutOperation != null) { 718 myTurnoutOperation.removePropertyChangeListener(this); 719 } 720 myTurnoutOperation = toper; 721 if (myTurnoutOperation != null) { 722 myTurnoutOperation.addPropertyChangeListener(this); 723 } 724 firePropertyChange(PROPERTY_TURNOUT_OPERATION_STATE, oldOp, myTurnoutOperation); 725 } 726 727 protected void operationPropertyChange(PropertyChangeEvent evt) { 728 if (evt.getSource() == myTurnoutOperation) { 729 if (((TurnoutOperation) evt.getSource()).isDeleted()) { 730 setTurnoutOperation(null); 731 } 732 } 733 } 734 735 /** {@inheritDoc} */ 736 @Override 737 public boolean getInhibitOperation() { 738 return inhibitOperation; 739 } 740 741 /** {@inheritDoc} */ 742 @Override 743 public void setInhibitOperation(boolean io) { 744 inhibitOperation = io; 745 } 746 747 /** 748 * Find the TurnoutOperation class for this turnout, and get an instance of 749 * the corresponding operator. Override this function if you want another way 750 * to choose the operation. 751 * 752 * @return newly-instantiated TurnoutOperator, or null if nothing suitable 753 */ 754 protected TurnoutOperator getTurnoutOperator() { 755 TurnoutOperator to = null; 756 if (!inhibitOperation) { 757 if (myTurnoutOperation != null) { 758 to = myTurnoutOperation.getOperator(this); 759 } else { 760 TurnoutOperation toper = InstanceManager.getDefault(TurnoutOperationManager.class) 761 .getMatchingOperation(this, 762 getFeedbackModeForOperation()); 763 if (toper != null) { 764 to = toper.getOperator(this); 765 } 766 } 767 } 768 return to; 769 } 770 771 /** 772 * Allow an actual turnout class to transform private feedback types into 773 * ones that the generic turnout operations know about. 774 * 775 * @return apparent feedback mode for operation lookup 776 */ 777 protected int getFeedbackModeForOperation() { 778 return getFeedbackMode(); 779 } 780 781 /** 782 * Support for associated sensor or sensors. 783 */ 784 private NamedBeanHandle<Sensor> _firstNamedSensor; 785 786 private NamedBeanHandle<Sensor> _secondNamedSensor; 787 788 /** {@inheritDoc} */ 789 @Override 790 public void provideFirstFeedbackSensor(String pName) throws JmriException, IllegalArgumentException { 791 if (InstanceManager.getNullableDefault(SensorManager.class) != null) { 792 if (pName == null || pName.isEmpty()) { 793 provideFirstFeedbackNamedSensor(null); 794 } else { 795 Sensor sensor = InstanceManager.sensorManagerInstance().provideSensor(pName); 796 provideFirstFeedbackNamedSensor(InstanceManager.getDefault(NamedBeanHandleManager.class) 797 .getNamedBeanHandle(pName, sensor)); 798 } 799 } else { 800 log.error("No SensorManager for this protocol"); 801 throw new JmriException("No Sensor Manager Found"); 802 } 803 } 804 805 /** 806 * On change, fires Property Change "TurnoutFeedbackFirstSensorChange". 807 * @param s the Handle for First Feedback Sensor 808 */ 809 public void provideFirstFeedbackNamedSensor(NamedBeanHandle<Sensor> s) { 810 // remove existing if any 811 Sensor temp = getFirstSensor(); 812 if (temp != null) { 813 temp.removePropertyChangeListener(this); 814 } 815 816 _firstNamedSensor = s; 817 818 // if need be, set listener 819 temp = getFirstSensor(); // might have changed 820 if (temp != null) { 821 temp.addPropertyChangeListener(this, s.getName(), "Feedback Sensor for " + getDisplayName()); 822 } 823 // set initial state 824 setInitialKnownStateFromFeedback(); 825 firePropertyChange(PROPERTY_TURNOUT_FEEDBACK_FIRST_SENSOR, temp, s); 826 } 827 828 /** {@inheritDoc} */ 829 @Override 830 public Sensor getFirstSensor() { 831 if (_firstNamedSensor == null) { 832 return null; 833 } 834 return _firstNamedSensor.getBean(); 835 } 836 837 /** {@inheritDoc} */ 838 @Override 839 public NamedBeanHandle<Sensor> getFirstNamedSensor() { 840 return _firstNamedSensor; 841 } 842 843 /** {@inheritDoc} */ 844 @Override 845 public void provideSecondFeedbackSensor(String pName) throws JmriException, IllegalArgumentException { 846 if (InstanceManager.getNullableDefault(SensorManager.class) != null) { 847 if (pName == null || pName.isEmpty()) { 848 provideSecondFeedbackNamedSensor(null); 849 } else { 850 Sensor sensor = InstanceManager.sensorManagerInstance().provideSensor(pName); 851 provideSecondFeedbackNamedSensor(InstanceManager.getDefault(NamedBeanHandleManager.class) 852 .getNamedBeanHandle(pName, sensor)); 853 } 854 } else { 855 log.error("No SensorManager for this protocol"); 856 throw new JmriException("No Sensor Manager Found"); 857 } 858 } 859 860 /** 861 * On change, fires Property Change "TurnoutFeedbackSecondSensorChange". 862 * @param s the Handle for Second Feedback Sensor 863 */ 864 public void provideSecondFeedbackNamedSensor(NamedBeanHandle<Sensor> s) { 865 // remove existing if any 866 Sensor temp = getSecondSensor(); 867 if (temp != null) { 868 temp.removePropertyChangeListener(this); 869 } 870 871 _secondNamedSensor = s; 872 873 // if need be, set listener 874 temp = getSecondSensor(); // might have changed 875 if (temp != null) { 876 temp.addPropertyChangeListener(this, s.getName(), "Feedback Sensor for " + getDisplayName()); 877 } 878 // set initial state 879 setInitialKnownStateFromFeedback(); 880 firePropertyChange(PROPERTY_TURNOUT_FEEDBACK_SECOND_SENSOR, temp, s); 881 } 882 883 /** {@inheritDoc} */ 884 @CheckForNull 885 @Override 886 public Sensor getSecondSensor() { 887 if (_secondNamedSensor == null) { 888 return null; 889 } 890 return _secondNamedSensor.getBean(); 891 } 892 893 /** {@inheritDoc} */ 894 @CheckForNull 895 @Override 896 public NamedBeanHandle<Sensor> getSecondNamedSensor() { 897 return _secondNamedSensor; 898 } 899 900 /** {@inheritDoc} */ 901 @Override 902 public void setInitialKnownStateFromFeedback() { 903 Sensor firstSensor = getFirstSensor(); 904 if (_activeFeedbackType == ONESENSOR) { 905 // ONESENSOR feedback 906 if (firstSensor != null) { 907 // set according to state of sensor 908 int sState = firstSensor.getKnownState(); 909 if (sState == Sensor.ACTIVE) { 910 newKnownState(THROWN); 911 } else if (sState == Sensor.INACTIVE) { 912 newKnownState(CLOSED); 913 } 914 } else { 915 log.warn("expected Sensor 1 not defined - {}", getSystemName()); 916 newKnownState(UNKNOWN); 917 } 918 } else if (_activeFeedbackType == TWOSENSOR) { 919 // TWOSENSOR feedback 920 int s1State = Sensor.UNKNOWN; 921 int s2State = Sensor.UNKNOWN; 922 if (firstSensor != null) { 923 s1State = firstSensor.getKnownState(); 924 } else { 925 log.warn("expected Sensor 1 not defined - {}", getSystemName()); 926 } 927 Sensor secondSensor = getSecondSensor(); 928 if (secondSensor != null) { 929 s2State = secondSensor.getKnownState(); 930 } else { 931 log.warn("expected Sensor 2 not defined - {}", getSystemName()); 932 } 933 // set Turnout state according to sensors 934 if ((s1State == Sensor.ACTIVE) && (s2State == Sensor.INACTIVE)) { 935 newKnownState(THROWN); 936 } else if ((s1State == Sensor.INACTIVE) && (s2State == Sensor.ACTIVE)) { 937 newKnownState(CLOSED); 938 } else if (_knownState != UNKNOWN) { 939 newKnownState(UNKNOWN); 940 } 941 // nothing required at this time for other modes 942 } 943 } 944 945 /** 946 * React to sensor changes by changing the KnownState if using an 947 * appropriate sensor mode. 948 */ 949 @Override 950 public void propertyChange(PropertyChangeEvent evt) { 951 if (evt.getSource() == myTurnoutOperation) { 952 operationPropertyChange(evt); 953 } else if (evt.getSource() == getFirstSensor() 954 || evt.getSource() == getSecondSensor()) { 955 sensorPropertyChange(evt); 956 } else if (evt.getSource() == leadingTurnout) { 957 leadingTurnoutPropertyChange(evt); 958 } 959 } 960 961 protected void sensorPropertyChange(PropertyChangeEvent evt) { 962 // top level, find the mode 963 Sensor src = (Sensor) evt.getSource(); 964 Sensor s1 = getFirstSensor(); 965 if (src == null || s1 == null) { 966 log.warn("Turnout feedback sensors configured incorrectly "); 967 return; // can't complete 968 } 969 970 if (_activeFeedbackType == ONESENSOR) { 971 // check for match 972 if (src == s1) { 973 // check change type 974 if (!PROPERTY_KNOWN_STATE.equals(evt.getPropertyName())) { 975 return; 976 } 977 // OK, now handle it 978 switch ((Integer) evt.getNewValue()) { 979 case Sensor.ACTIVE: 980 newKnownState(THROWN); 981 break; 982 case Sensor.INACTIVE: 983 newKnownState(CLOSED); 984 break; 985 default: 986 newKnownState(INCONSISTENT); 987 break; 988 } 989 } else { 990 // unexpected mismatch 991 NamedBeanHandle<Sensor> firstNamed = getFirstNamedSensor(); 992 if (firstNamed != null) { 993 log.warn("expected sensor {} was {}", firstNamed.getName(), src.getSystemName()); 994 } else { 995 log.error("unexpected (null) sensors"); 996 } 997 } 998 // end ONESENSOR block 999 } else if (_activeFeedbackType == TWOSENSOR) { 1000 // check change type 1001 if (!PROPERTY_KNOWN_STATE.equals(evt.getPropertyName())) { 1002 return; 1003 } 1004 // OK, now handle it 1005 Sensor s2 = getSecondSensor(); 1006 if (s2 == null) { 1007 log.warn("Turnout feedback sensor 2 configured incorrectly "); 1008 return; // can't complete 1009 } 1010 if (s1.getKnownState() == Sensor.INACTIVE && s2.getKnownState() == Sensor.ACTIVE) { 1011 newKnownState(CLOSED); 1012 } else if (s1.getKnownState() == Sensor.ACTIVE && s2.getKnownState() == Sensor.INACTIVE) { 1013 newKnownState(THROWN); 1014 } else if (s1.getKnownState() == Sensor.UNKNOWN && s2.getKnownState() == Sensor.UNKNOWN) { 1015 newKnownState(UNKNOWN); 1016 } else { 1017 newKnownState(INCONSISTENT); 1018 } 1019 // end TWOSENSOR block 1020 } 1021 } 1022 1023 protected void leadingTurnoutPropertyChange(PropertyChangeEvent evt) { 1024 int state = (int) evt.getNewValue(); 1025 if (PROPERTY_KNOWN_STATE.equals(evt.getPropertyName()) 1026 && leadingTurnout != null) { 1027 if (followingCommandedState || state != leadingTurnout.getCommandedState()) { 1028 newKnownState(state); 1029 } else { 1030 newKnownState(getCommandedState()); 1031 } 1032 } 1033 } 1034 1035 /** {@inheritDoc} */ 1036 @Override 1037 public void setBinaryOutput(boolean state) { 1038 binaryOutput = true; 1039 } 1040 protected boolean binaryOutput = false; 1041 1042 /** {@inheritDoc} */ 1043 @Override 1044 public void dispose() { 1045 Sensor temp; 1046 temp = getFirstSensor(); 1047 if (temp != null) { 1048 temp.removePropertyChangeListener(this); 1049 } 1050 _firstNamedSensor = null; 1051 temp = getSecondSensor(); 1052 if (temp != null) { 1053 temp.removePropertyChangeListener(this); 1054 } 1055 _secondNamedSensor = null; 1056 super.dispose(); 1057 } 1058 1059 private String _divergeSpeed = ""; 1060 private String _straightSpeed = ""; 1061 1062 /** {@inheritDoc} */ 1063 @Override 1064 public float getDivergingLimit() { 1065 if ((_divergeSpeed == null) || (_divergeSpeed.isEmpty())) { 1066 return -1; 1067 } 1068 1069 String speed = _divergeSpeed; 1070 if (_divergeSpeed.equals("Global")) { 1071 speed = InstanceManager.turnoutManagerInstance().getDefaultThrownSpeed(); 1072 } 1073 if (speed.equals("Block")) { 1074 return -1; 1075 } 1076 try { 1077 return Float.parseFloat(speed); 1078 } catch (NumberFormatException nx) { 1079 //considered normal if the speed is not a number. 1080 } 1081 try { 1082 return InstanceManager.getDefault(SignalSpeedMap.class).getSpeed(speed); 1083 } catch (IllegalArgumentException ex) { 1084 return -1; 1085 } 1086 } 1087 1088 /** {@inheritDoc} */ 1089 @Override 1090 public String getDivergingSpeed() { 1091 if (_divergeSpeed.equals("Global")) { 1092 return (Bundle.getMessage("UseGlobal", "Global") + " " + 1093 InstanceManager.turnoutManagerInstance().getDefaultThrownSpeed()); 1094 } 1095 if (_divergeSpeed.equals("Block")) { 1096 return (Bundle.getMessage("UseGlobal", "Block Speed")); 1097 } 1098 return _divergeSpeed; 1099 } 1100 1101 /** 1102 * {@inheritDoc} 1103 * On change, fires Property Change "TurnoutDivergingSpeedChange". 1104 */ 1105 @Override 1106 public void setDivergingSpeed(String s) throws JmriException { 1107 if (s == null) { 1108 throw new JmriException("Value of requested turnout thrown speed can not be null"); 1109 } 1110 if (_divergeSpeed.equals(s)) { 1111 return; 1112 } 1113 if (s.contains("Global")) { 1114 s = "Global"; 1115 } else if (s.contains("Block")) { 1116 s = "Block"; 1117 } else { 1118 try { 1119 Float.parseFloat(s); 1120 } catch (NumberFormatException nx) { 1121 try { 1122 InstanceManager.getDefault(SignalSpeedMap.class).getSpeed(s); 1123 } catch (IllegalArgumentException ex) { 1124 throw new JmriException("Value of requested block speed is not valid"); 1125 } 1126 } 1127 } 1128 String oldSpeed = _divergeSpeed; 1129 _divergeSpeed = s; 1130 firePropertyChange(PROPERTY_TURNOUT_DIVERGING_SPEED, oldSpeed, s); 1131 } 1132 1133 /** {@inheritDoc} */ 1134 @Override 1135 public float getStraightLimit() { 1136 if ((_straightSpeed == null) || (_straightSpeed.isEmpty())) { 1137 return -1; 1138 } 1139 String speed = _straightSpeed; 1140 if (_straightSpeed.equals("Global")) { 1141 speed = InstanceManager.turnoutManagerInstance().getDefaultClosedSpeed(); 1142 } 1143 if (speed.equals("Block")) { 1144 return -1; 1145 } 1146 try { 1147 return Float.parseFloat(speed); 1148 } catch (NumberFormatException nx) { 1149 //considered normal if the speed is not a number. 1150 } 1151 try { 1152 return InstanceManager.getDefault(SignalSpeedMap.class).getSpeed(speed); 1153 } catch (IllegalArgumentException ex) { 1154 return -1; 1155 } 1156 } 1157 1158 /** {@inheritDoc} */ 1159 @Override 1160 public String getStraightSpeed() { 1161 if (_straightSpeed.equals("Global")) { 1162 return (Bundle.getMessage("UseGlobal", "Global") + " " + 1163 InstanceManager.turnoutManagerInstance().getDefaultClosedSpeed()); 1164 } 1165 if (_straightSpeed.equals("Block")) { 1166 return (Bundle.getMessage("UseGlobal", "Block Speed")); 1167 } 1168 return _straightSpeed; 1169 } 1170 1171 /** 1172 * {@inheritDoc} 1173 * On change, fires Property Change "TurnoutStraightSpeedChange". 1174 */ 1175 @Override 1176 public void setStraightSpeed(String s) throws JmriException { 1177 if (s == null) { 1178 throw new JmriException("Value of requested turnout straight speed can not be null"); 1179 } 1180 if (_straightSpeed.equals(s)) { 1181 return; 1182 } 1183 if (s.contains("Global")) { 1184 s = "Global"; 1185 } else if (s.contains("Block")) { 1186 s = "Block"; 1187 } else { 1188 try { 1189 Float.parseFloat(s); 1190 } catch (NumberFormatException nx) { 1191 try { 1192 InstanceManager.getDefault(SignalSpeedMap.class).getSpeed(s); 1193 } catch (IllegalArgumentException ex) { 1194 throw new JmriException("Value of requested turnout straight speed is not valid"); 1195 } 1196 } 1197 } 1198 String oldSpeed = _straightSpeed; 1199 _straightSpeed = s; 1200 firePropertyChange(PROPERTY_TURNOUT_STRAIGHT_SPEED, oldSpeed, s); 1201 } 1202 1203 /** {@inheritDoc} */ 1204 @Override 1205 public void vetoableChange(PropertyChangeEvent evt) throws PropertyVetoException { 1206 if ( Manager.PROPERTY_CAN_DELETE.equals(evt.getPropertyName())) { 1207 Object old = evt.getOldValue(); 1208 if (old.equals(getFirstSensor()) || old.equals(getSecondSensor()) || old.equals(leadingTurnout)) { 1209 PropertyChangeEvent e = new PropertyChangeEvent( 1210 this, Manager.PROPERTY_DO_NOT_DELETE, null, null); 1211 throw new PropertyVetoException( 1212 Bundle.getMessage("InUseSensorTurnoutVeto", getDisplayName()), e); // NOI18N 1213 } 1214 } 1215 } 1216 1217 /** {@inheritDoc} */ 1218 @Override 1219 public List<NamedBeanUsageReport> getUsageReport(NamedBean bean) { 1220 List<NamedBeanUsageReport> report = new ArrayList<>(); 1221 if (bean != null) { 1222 if (bean.equals(getFirstSensor())) { 1223 report.add(new NamedBeanUsageReport("TurnoutFeedback1")); // NOI18N 1224 } 1225 if (bean.equals(getSecondSensor())) { 1226 report.add(new NamedBeanUsageReport("TurnoutFeedback2")); // NOI18N 1227 } 1228 if (bean.equals(getLeadingTurnout())) { 1229 report.add(new NamedBeanUsageReport("LeadingTurnout")); // NOI18N 1230 } 1231 } 1232 return report; 1233 } 1234 1235 /** 1236 * {@inheritDoc} 1237 */ 1238 @Override 1239 public boolean isCanFollow() { 1240 return false; 1241 } 1242 1243 /** 1244 * {@inheritDoc} 1245 */ 1246 @Override 1247 @CheckForNull 1248 public Turnout getLeadingTurnout() { 1249 return leadingTurnout; 1250 } 1251 1252 /** 1253 * {@inheritDoc} 1254 */ 1255 @Override 1256 public void setLeadingTurnout(@CheckForNull Turnout turnout) { 1257 if (isCanFollow()) { 1258 Turnout old = leadingTurnout; 1259 leadingTurnout = turnout; 1260 firePropertyChange(PROPERTY_LEADING_TURNOUT, old, leadingTurnout); 1261 if (old != null) { 1262 old.removePropertyChangeListener(PROPERTY_KNOWN_STATE, this); 1263 } 1264 if (leadingTurnout != null) { 1265 leadingTurnout.addPropertyChangeListener(PROPERTY_KNOWN_STATE, this); 1266 } 1267 } 1268 } 1269 1270 /** 1271 * {@inheritDoc} 1272 */ 1273 @Override 1274 public void setLeadingTurnout(@CheckForNull Turnout turnout, boolean followingCommandedState) { 1275 setLeadingTurnout(turnout); 1276 setFollowingCommandedState(followingCommandedState); 1277 } 1278 1279 /** 1280 * {@inheritDoc} 1281 */ 1282 @Override 1283 public boolean isFollowingCommandedState() { 1284 return followingCommandedState; 1285 } 1286 1287 /** 1288 * {@inheritDoc} 1289 */ 1290 @Override 1291 public void setFollowingCommandedState(boolean following) { 1292 followingCommandedState = following; 1293 } 1294 1295 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(AbstractTurnout.class); 1296 1297}