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