001/* 002 * @author Gregory J. Bedlek Copyright (C) 2018, 2019 003 */ 004package jmri.jmrit.ctc; 005 006import java.beans.PropertyChangeEvent; 007import java.beans.PropertyChangeListener; 008import java.util.ArrayList; 009import jmri.Sensor; 010import jmri.Turnout; 011import jmri.jmrit.ctc.ctcserialdata.CodeButtonHandlerData; 012 013/** 014This only works with Digitrax DS54's and DS64's configured to LOCALLY change the switch via either 015a pushbutton or toggle switch. Specifically in JMRI / DS64 programmer, OpSw 21 SHOULD 016be checked. DS54's should be similarly configured. 017 018The other way: 019One would NOT check OpSw21, and then one would have to write JMRI software (or a script) 020to process the message from the DS54/DS64, and then send the appropriate turnout 021"CLOSED/THROWN" command to the turnout to effect the change. 022 023Advantage: No turnout movement when crew requests change unless allowed by Dispatcher. 024Disadvantage: Software MUST be running in order to throw turnout "normally". In other 025 words cannot run the layout without the computer, and with all turnouts 026 controlled by the dispatcher set to "local control". 027 028This modules way: 029The purpose of this module is to "countermand" attempts by the field crews to throw 030a switch that is under dispatcher control. This works ONLY for switches that have feedback. 031 032Advantage: Computer NOT necessary to throw switch. 033Disadvantage: Switch will "partially throw" until the feedback contact changes and sends 034 a message to the software, which then countermands it. If a train is on the 035 switch, it may be derailed. 036 037NOTES: 038 If a route is cleared thru, you MUST be prevented from UNLOCKING a locked switch. 039Failure to provide such an object will just allow unlocking while a route is cleared thru. 040 041If dispatcherSensorLockToggle is None, then INSURE that you call "ExternalLockTurnout" at some 042point to lock the turnout, since this starts up with the turnout unlocked! 043 044* See the documentation for the matrix regarding Command and Feedback normal/reversed. 045 */ 046 047public class TurnoutLock { 048 private final NBHSensor _mDispatcherSensorLockToggle; 049 private int _mCommandedState = Turnout.CLOSED; // Assume 050 private ArrayList<NBHTurnout> _mTurnoutsMonitored = new ArrayList<>(); 051 private PropertyChangeListener _mTurnoutsMonitoredPropertyChangeListener = null; 052 private boolean _mLocked = false; 053 private NBHSensor _mDispatcherSensorUnlockedIndicator; 054 private boolean _mNoDispatcherControlOfSwitch = false; 055 private int _m_ndcos_WhenLockedSwitchState = 0; 056 057 public TurnoutLock( String userIdentifier, 058 NBHSensor dispatcherSensorLockToggle, // Toggle switch that indicates lock/unlock on the panel. If None, then PERMANENTLY locked by the Dispatcher! 059 NBHTurnout actualTurnout, // The turnout being locked: LTxx a real turnout, like LT69. 060 boolean actualTurnoutFeedbackDifferent, // True / False, in case feedback backwards but switch command above isn't! 061 NBHSensor dispatcherSensorUnlockedIndicator, // Display unlocked status (when ACTIVE) back to the Dispatcher. 062 boolean noDispatcherControlOfSwitch, // Dispatcher doesn't control the switch. If TRUE, then provide: 063 int ndcos_WhenLockedSwitchState, // When Dispatcher does lock, switch should be set to: CLOSED/THROWN 064 CodeButtonHandlerData.LOCK_IMPLEMENTATION _mLockImplementation, // Someday, choose which one to implement. Right now, my own. 065 boolean turnoutLocksEnabledAtStartup, 066 NBHTurnout additionalTurnout1, 067 boolean additionalTurnout1FeebackReversed, 068 NBHTurnout additionalTurnout2, 069 boolean additionalTurnout2FeebackReversed, 070 NBHTurnout additionalTurnout3, 071 boolean additionalTurnout3FeebackReversed) { 072 _mDispatcherSensorLockToggle = dispatcherSensorLockToggle; 073 addTurnoutMonitored(userIdentifier, "actualTurnout", actualTurnout, actualTurnoutFeedbackDifferent, true); 074 _mDispatcherSensorUnlockedIndicator = dispatcherSensorUnlockedIndicator; 075 _mDispatcherSensorLockToggle.setKnownState(turnoutLocksEnabledAtStartup ? Sensor.INACTIVE : Sensor.ACTIVE); 076 _mNoDispatcherControlOfSwitch = noDispatcherControlOfSwitch; 077 _m_ndcos_WhenLockedSwitchState = ndcos_WhenLockedSwitchState; 078 addTurnoutMonitored(userIdentifier, "additionalTurnout1", additionalTurnout1, additionalTurnout1FeebackReversed, false); // NOI18N 079 addTurnoutMonitored(userIdentifier, "additionalTurnout2", additionalTurnout2, additionalTurnout2FeebackReversed, false); // NOI18N 080 addTurnoutMonitored(userIdentifier, "additionalTurnout3", additionalTurnout3, additionalTurnout3FeebackReversed, false); // NOI18N 081 updateDispatcherSensorIndicator(turnoutLocksEnabledAtStartup); 082 _mTurnoutsMonitoredPropertyChangeListener = (PropertyChangeEvent e) -> { handleTurnoutChange(e); }; 083 for (NBHTurnout tempTurnout : _mTurnoutsMonitored) { 084 if (tempTurnout.getKnownState() == Turnout.UNKNOWN) { 085 tempTurnout.setCommandedState(_mCommandedState); // MUST be done before "addPropertyChangeListener": 086 } 087 tempTurnout.addPropertyChangeListener(_mTurnoutsMonitoredPropertyChangeListener); 088 } 089 } 090 091 public void removeAllListeners() { 092 for (NBHTurnout tempTurnout : _mTurnoutsMonitored) { 093 tempTurnout.removePropertyChangeListener(_mTurnoutsMonitoredPropertyChangeListener); 094 } 095 } 096 097 public NBHSensor getDispatcherSensorLockToggle() { return _mDispatcherSensorLockToggle; } 098 099 private void addTurnoutMonitored(String userIdentifier, String parameter, NBHTurnout actualTurnout, boolean FeedbackDifferent, boolean required) { 100 boolean actualTurnoutPresent = actualTurnout.valid(); 101 if (required && !actualTurnoutPresent) { 102 (new CTCException("TurnoutLock", userIdentifier, parameter, Bundle.getMessage("RequiredTurnoutMissing"))).logError(); // NOI18N 103 return; 104 } 105 if (actualTurnoutPresent) { // IF there is something there, try it: 106 if (actualTurnout.valid()) _mTurnoutsMonitored.add(actualTurnout); 107 } 108 } 109 110// Was propertyChange: 111 private void handleTurnoutChange(PropertyChangeEvent e) { 112 if (e.getPropertyName().equals("KnownState")) { // NOI18N 113 if (_mLocked) { // Act on locked only! 114 NBHTurnout turnout = null; // Not found. 115 for (int index = 0; index < _mTurnoutsMonitored.size(); index++) { // Find matching entry: 116 if (e.getSource() == _mTurnoutsMonitored.get(index).getBean()) { // Matched: 117 turnout = _mTurnoutsMonitored.get(index); 118 break; 119 } 120 } 121 if (turnout != null) { // Safety check: 122 if (_mCommandedState != turnout.getKnownState()) { // Someone in the field messed with it: 123 turnoutSetCommandedState(turnout, _mCommandedState); // Just directly restore it 124 } 125 } 126 } 127 } 128 } 129 130/* 131External software calls this from initialization code in order to lock the turnout. When this code starts 132up the lock status is UNLOCKED so that initialization code can do whatever to the turnout. 133This routine DOES NOT modify the state of the switch, ONLY the lock! 134*/ 135 public void externalLockTurnout() { 136 _mDispatcherSensorLockToggle.setKnownState(Sensor.INACTIVE); 137 updateDispatcherSensorIndicator(true); 138 } 139 140// Ditto above routine, except opposite: 141 public void externalUnlockTurnout() { 142 _mDispatcherSensorLockToggle.setKnownState(Sensor.ACTIVE); 143 updateDispatcherSensorIndicator(false); 144 } 145 146// External software calls this (from CodeButtonHandler typically) to inform us of a valid code button push: 147 public void codeButtonPressed() { 148 boolean newLockedState = getNewLockedState(); 149 if (newLockedState == _mLocked) return; // Nothing changed 150// The PROTOTYPE would not do this: Since the dispatcher CANNOT control the state of the switch, and 151// our operating crews (for example: "brains go dead going down the stairs") MAY forget to normalize the switch 152// for the main (for instance), we FORCE the state of the switch(s) to a known state (hopefully for the main) 153 if (_mNoDispatcherControlOfSwitch && newLockedState == true) { // No dispatcher control of switch and LOCKING them, "normalize" the switch: 154 for (NBHTurnout turnout : _mTurnoutsMonitored) { 155 turnoutSetCommandedState(turnout, _m_ndcos_WhenLockedSwitchState); // Make it so. 156 } 157 } 158 updateDispatcherSensorIndicator(newLockedState); 159 } 160 161// External software calls this (from CodeButtonHandler typically) to tell us of the new state of the turnout: 162 public void dispatcherCommandedState(int commandedState) { 163 if (commandedState == CTCConstants.SWITCHNORMAL) _mCommandedState = Turnout.CLOSED; else _mCommandedState = Turnout.THROWN; 164 } 165 166 public boolean turnoutPresentlyLocked() { return _mLocked; } 167 168 public boolean getNewLockedState() { 169 return _mDispatcherSensorLockToggle.getKnownState() == Sensor.INACTIVE; 170 } 171 172 public boolean tryingToChangeLockStatus() { return getNewLockedState() != _mLocked; } 173 174 private void turnoutSetCommandedState(NBHTurnout turnout, int state) { 175 _mCommandedState = state; 176 turnout.setCommandedState(state); 177 } 178 179 private void updateDispatcherSensorIndicator(boolean newLockedState) { 180 _mLocked = newLockedState; 181 _mDispatcherSensorUnlockedIndicator.setKnownState(_mLocked ? Sensor.INACTIVE : Sensor.ACTIVE); 182 } 183}