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