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