001package jmri.jmrit.ctc; 002import java.beans.PropertyChangeEvent; 003import java.beans.PropertyChangeListener; 004import java.util.HashSet; 005import jmri.*; 006import org.slf4j.LoggerFactory; 007 008/** 009 * This is the "master" class that handles everything when a code button is 010 * pressed. As such, it has a LOT of external data passed into it's constructor, 011 * and operates and modifies all objects it contains on a dynamic basis both 012 * when the button is pressed, and when external events happen that affect this 013 * object. 014 * <p> 015 * Notes: 016 * <p> 017 * Changing both signal direction to non signals normal and switch direction at the same time "is allowed". 018 * Lock/Unlock is the LOWEST priority! Call on is the HIGHEST priority. 019 * <p> 020 * As of V1.04 of the CTC system, preconditioning (a.k.a. stacking) is supported. It is enabled 021 * by setting the internal sensor (automatically created) "IS:PRECONDITIONING_ENABLED" to active. 022 * Any other value inactivates this feature. For example, the user can create a toggle 023 * switch to activate / inactivate it. 024 * 025 * @author Gregory J. Bedlek Copyright (C) 2018, 2019, 2020 026 */ 027public class CodeButtonHandler { 028 private final boolean _mTurnoutLockingOnlyEnabled; 029 private final LockedRoutesManager _mLockedRoutesManager; 030 private final String _mUserIdentifier; 031 private final int _mUniqueID; 032 private final NBHSensor _mCodeButtonInternalSensor; 033 private final PropertyChangeListener _mCodeButtonInternalSensorPropertyChangeListener; 034 private final NBHSensor _mOSSectionOccupiedExternalSensor; 035 private final NBHSensor _mOSSectionOccupiedExternalSensor2; 036 private final PropertyChangeListener _mOSSectionOccupiedExternalSensorPropertyChangeListener; 037 private final SignalDirectionIndicatorsInterface _mSignalDirectionIndicators; 038 private final SignalDirectionLever _mSignalDirectionLever; 039 private final SwitchDirectionIndicators _mSwitchDirectionIndicators; 040 private final SwitchDirectionLever _mSwitchDirectionLever; 041 private final Fleeting _mFleeting; 042 private final CallOn _mCallOn; 043 private final TrafficLocking _mTrafficLocking; 044 private final TurnoutLock _mTurnoutLock; 045 private final IndicationLockingSignals _mIndicationLockingSignals; 046 private final CodeButtonSimulator _mCodeButtonSimulator; 047 private LockedRoute _mLockedRoute = null; 048 049 private static final Sensor _mPreconditioningEnabledSensor = initializePreconditioningEnabledSensor(); 050 private static class PreconditioningData { 051 public boolean _mCodeButtonPressed = false; // If false, values in these don't matter: 052 public int _mSignalDirectionLeverWas = CTCConstants.OUTOFCORRESPONDENCE; // Safety: 053 public int _mSwitchDirectionLeverWas = CTCConstants.OUTOFCORRESPONDENCE; 054 } 055 056 private static Sensor initializePreconditioningEnabledSensor() { 057 Sensor returnValue = InstanceManager.sensorManagerInstance().newSensor("IS:PRECONDITIONING_ENABLED", null); // NOI18N 058 int knownState = returnValue.getKnownState(); 059 if (Sensor.ACTIVE != knownState && Sensor.INACTIVE != knownState) { 060 try {returnValue.setKnownState(Sensor.INACTIVE); } catch (JmriException ex) { 061 LoggerFactory.getLogger(CodeButtonHandler.class).debug("Sensor problem, preconditioning won't work."); // NOI18N 062 } 063 } 064 return returnValue; 065 } 066 private PreconditioningData _mPreconditioningData = new PreconditioningData(); 067 068 public CodeButtonHandler( boolean turnoutLockingOnlyEnabled, // If this is NOT an O.S. section, but only a turnout lock, then this is true. 069 LockedRoutesManager lockedRoutesManager, 070 String userIdentifier, 071 int uniqueID, 072 NBHSensor codeButtonInternalSensor, // Required 073 int codeButtonDelayInMilliseconds, // If 0, REAL code button, if > 0, tower operations (simulated code button). 074 NBHSensor osSectionOccupiedExternalSensor, // Required, if ACTIVE prevents turnout, lock or call on from occuring. 075 NBHSensor osSectionOccupiedExternalSensor2, // Optional, if ACTIVE prevents turnout, lock or call on from occuring. 076 SignalDirectionIndicatorsInterface signalDirectionIndicators, // Required 077 SignalDirectionLever signalDirectionLever, 078 SwitchDirectionIndicators switchDirectionIndicators, 079 SwitchDirectionLever switchDirectionLever, 080 Fleeting fleeting, // If null, then ALWAYS fleeting! 081 CallOn callOn, 082 TrafficLocking trafficLocking, 083 TurnoutLock turnoutLock, 084 IndicationLockingSignals indicationLockingSignals) { // Needed for check of adjacent OS Section(s), and optionally turnoutLock. 085 signalDirectionIndicators.setCodeButtonHandler(this); 086 _mTurnoutLockingOnlyEnabled = turnoutLockingOnlyEnabled; 087 _mLockedRoutesManager = lockedRoutesManager; 088 _mUserIdentifier = userIdentifier; 089 _mUniqueID = uniqueID; 090 _mSignalDirectionIndicators = signalDirectionIndicators; 091 _mSignalDirectionLever = signalDirectionLever; 092 _mSwitchDirectionIndicators = switchDirectionIndicators; 093 _mSwitchDirectionLever = switchDirectionLever; 094 _mFleeting = fleeting; 095 _mCallOn = callOn; 096 _mTrafficLocking = trafficLocking; 097 _mTurnoutLock = turnoutLock; 098 _mIndicationLockingSignals = indicationLockingSignals; 099 _mCodeButtonInternalSensor = codeButtonInternalSensor; 100 _mCodeButtonInternalSensor.setKnownState(Sensor.INACTIVE); 101 _mCodeButtonInternalSensorPropertyChangeListener = (PropertyChangeEvent e) -> { codeButtonStateChange(e); }; 102 _mCodeButtonInternalSensor.addPropertyChangeListener(_mCodeButtonInternalSensorPropertyChangeListener); 103 104 _mOSSectionOccupiedExternalSensorPropertyChangeListener = (PropertyChangeEvent e) -> { osSectionPropertyChangeEvent(e); }; 105 _mOSSectionOccupiedExternalSensor = osSectionOccupiedExternalSensor; 106 _mOSSectionOccupiedExternalSensor.addPropertyChangeListener(_mOSSectionOccupiedExternalSensorPropertyChangeListener); 107 108// NO property change for this, only used for turnout locking: 109 _mOSSectionOccupiedExternalSensor2 = osSectionOccupiedExternalSensor2; 110 111 if (codeButtonDelayInMilliseconds > 0) { // SIMULATED code button: 112 _mCodeButtonSimulator = new CodeButtonSimulator(codeButtonDelayInMilliseconds, 113 _mCodeButtonInternalSensor, 114 _mSwitchDirectionLever, 115 _mSignalDirectionLever, 116 _mTurnoutLock); 117 } else { 118 _mCodeButtonSimulator = null; 119 } 120 } 121 122 /** 123 * This routine SHOULD ONLY be called by CTCMain when the CTC system is shutdown 124 * in order to clean up all resources prior to a restart. Nothing else should 125 * call this. 126 */ 127 public void removeAllListeners() { 128// Remove our registered listeners first: 129 _mCodeButtonInternalSensor.removePropertyChangeListener(_mCodeButtonInternalSensorPropertyChangeListener); 130 _mOSSectionOccupiedExternalSensor.removePropertyChangeListener(_mOSSectionOccupiedExternalSensorPropertyChangeListener); 131// Give each object a chance to remove theirs also: 132 if (_mSignalDirectionIndicators != null) _mSignalDirectionIndicators.removeAllListeners(); 133 if (_mSignalDirectionLever != null) _mSignalDirectionLever.removeAllListeners(); 134 if (_mSwitchDirectionIndicators != null) _mSwitchDirectionIndicators.removeAllListeners(); 135 if (_mSwitchDirectionLever != null) _mSwitchDirectionLever.removeAllListeners(); 136 if (_mFleeting != null) _mFleeting.removeAllListeners(); 137 if (_mCallOn != null) _mCallOn.removeAllListeners(); 138 if (_mTrafficLocking != null) _mTrafficLocking.removeAllListeners(); 139 if (_mTurnoutLock != null) _mTurnoutLock.removeAllListeners(); 140 if (_mIndicationLockingSignals != null) _mIndicationLockingSignals.removeAllListeners(); 141 if (_mCodeButtonSimulator != null) _mCodeButtonSimulator.removeAllListeners(); 142 } 143 144 /** 145 * SignalDirectionIndicators calls us here when time locking is done. 146 */ 147 public void cancelLockedRoute() { 148 _mLockedRoutesManager.cancelLockedRoute(_mLockedRoute); // checks passed parameter for null for us 149 _mLockedRoute = null; // Not valid anymore. 150 } 151 152 public boolean uniqueIDMatches(int uniqueID) { return _mUniqueID == uniqueID; } 153 public NBHSensor getOSSectionOccupiedExternalSensor() { return _mOSSectionOccupiedExternalSensor; } 154 155 private void osSectionPropertyChangeEvent(PropertyChangeEvent e) { 156 if (isPrimaryOSSectionOccupied()) { // MUST ALWAYS process PRIMARY OS occupied state change to ACTIVE (It's the only one that comes here anyways!) 157 if (_mFleeting != null && !_mFleeting.isFleetingEnabled()) { // Impliment "stick" here: 158 _mSignalDirectionIndicators.forceAllSignalsToHeld(); 159 } 160 _mSignalDirectionIndicators.osSectionBecameOccupied(); 161 } 162 else { // Process pre-conditioning if available: 163 if (_mPreconditioningData._mCodeButtonPressed) { 164 doCodeButtonPress(); 165 _mPreconditioningData._mCodeButtonPressed = false; 166 } 167 } 168 } 169 170 public void externalLockTurnout() { 171 if (_mTurnoutLock != null) _mTurnoutLock.externalLockTurnout(); 172 } 173 174 private void codeButtonStateChange(PropertyChangeEvent e) { 175 if (e.getPropertyName().equals("KnownState") && (int)e.getNewValue() == Sensor.ACTIVE) { 176// NOTE: If the primary O.S. section is occupied, you CANT DO ANYTHING via a CTC machine, except: 177// Preconditioning: IF the O.S. section is occupied, then it is a pre-conditioning request: 178 if (isPrimaryOSSectionOccupied()) { 179 if (Sensor.ACTIVE == _mPreconditioningEnabledSensor.getKnownState()) { // ONLY if turned on: 180 _mPreconditioningData._mSignalDirectionLeverWas = getCurrentSignalDirectionLever(false); 181 _mPreconditioningData._mSwitchDirectionLeverWas = getSwitchDirectionLeverRequestedState(false); 182 _mPreconditioningData._mCodeButtonPressed = true; // Do this LAST so that the above variables are stable in this object, 183 // in case there is a multi-threading issue (yea, lock it would be better, 184 // but this is good enough for now!) 185 } 186 } 187 doCodeButtonPress(); 188 } 189 } 190 191 private void doCodeButtonPress() { 192 if (_mSignalDirectionIndicators.isRunningTime()) return; // If we are running time, IGNORE all requests from the user: 193 possiblyAllowLockChange(); // MUST unlock first, otherwise if dispatcher wanted to unlock and change switch state, it wouldn't! 194 possiblyAllowTurnoutChange(); // Change turnout 195// IF the call on was accepted, then we DON'T attempt to change the signals to a more favorable 196// aspect here. Additionally see the comments above CallOn.java/"codeButtonPressed" for an explanation 197// of a "fake out" that happens in that routine, and it's effect on this code here: 198 if (!possiblyAllowCallOn()) { // NO call on occured or was allowed or requested: 199 possiblyAllowSignalDirectionChange(); // Slave to it! 200 } 201 } 202 203// Returns true if call on was actually done, else false 204 private boolean possiblyAllowCallOn() { 205 boolean returnStatus = false; 206 if (allowCallOnChange()) { 207 HashSet<Sensor> sensors = new HashSet<>(); // Initial O.S. section sensor(s): 208 sensors.add(_mOSSectionOccupiedExternalSensor.getBean()); // Always. 209// If there is a switch direction indicator, and it is reversed, then add the other sensor if valid here: 210 if (_mSwitchDirectionIndicators != null && _mSwitchDirectionIndicators.getLastIndicatorState() == CTCConstants.SWITCHREVERSED) { 211 if (_mOSSectionOccupiedExternalSensor2.valid()) sensors.add(_mOSSectionOccupiedExternalSensor2.getBean()); 212 } 213// NOTE: We DO NOT support preconditioning of call on, ergo false passed to "getCurrentSignalDirectionLever" 214 TrafficLockingInfo trafficLockingInfo = _mCallOn.codeButtonPressed(sensors, _mUserIdentifier, _mSignalDirectionIndicators, getCurrentSignalDirectionLever(false)); 215 if (trafficLockingInfo._mLockedRoute != null) { // Was allocated: 216 _mLockedRoute = trafficLockingInfo._mLockedRoute; 217 } 218 returnStatus = trafficLockingInfo._mReturnStatus; 219 } 220 if (_mCallOn != null) _mCallOn.resetToggle(); 221 return returnStatus; 222 } 223 224/* 225Rules from http://www.ctcparts.com/about.htm 226 "An important note though for programming logic is that the interlocking limits 227must be clear and all power switches within the interlocking limits aligned 228appropriately for the back to train route for this feature to activate." 229*/ 230 private boolean allowCallOnChange() { 231// Safety checks: 232 if (_mCallOn == null) return false; 233// Rules: 234 if (isPrimaryOSSectionOccupied()) return false; 235 if (_mSignalDirectionIndicators.isRunningTime()) return false; 236 if (_mSignalDirectionIndicators.getSignalsInTheFieldDirection() != CTCConstants.SIGNALSNORMAL) return false; 237 if (!areOSSensorsAvailableInRoutes()) return false; 238 return true; 239 } 240 241// If it doesn't exist, this returns OUTOFCORRESPONDENCE, else return it's present state: 242// NOTE: IF a preconditioned input was available, it OVERRIDES actual Signal Direction Lever (which is ignored in this case). 243 private int getCurrentSignalDirectionLever(boolean allowMergeInPreconditioning) { 244 if (_mSignalDirectionLever == null) return CTCConstants.OUTOFCORRESPONDENCE; 245 if (allowMergeInPreconditioning && _mPreconditioningData._mCodeButtonPressed) { // We can check and it is available: 246 if (_mPreconditioningData._mSignalDirectionLeverWas == CTCConstants.LEFTTRAFFIC 247 || _mPreconditioningData._mSignalDirectionLeverWas == CTCConstants.RIGHTTRAFFIC) { // Was valid: 248 return _mPreconditioningData._mSignalDirectionLeverWas; 249 } 250 } 251 return _mSignalDirectionLever.getPresentSignalDirectionLeverState(); 252 } 253 254 private void possiblyAllowTurnoutChange() { 255 if (allowTurnoutChange()) { 256 int requestedState = getSwitchDirectionLeverRequestedState(true); 257 notifyTurnoutLockObjectOfNewAlignment(requestedState); // Tell lock object this is new alignment 258 if (_mSwitchDirectionIndicators != null) { // Safety: 259 _mSwitchDirectionIndicators.codeButtonPressed(requestedState); // Also sends commmands to move the points 260 } 261 } 262 } 263 264 private boolean allowTurnoutChange() { 265// Safety checks: 266// Rules: 267 if (!_mSignalDirectionIndicators.signalsNormal()) return false; 268 if (routeClearedAcross()) return false; // Something was cleared thru, NO CHANGE 269 if (isEitherOSSectionOccupied()) return false; 270// 6/28/16: If the switch direction indicators are presently "OUTOFCORRESPONDENCE", IGNORE request, as we are presently working on a change: 271 if (!switchDirectionIndicatorsInCorrespondence()) return false; 272 if (!turnoutPresentlyLocked()) return false; 273 if (!areOSSensorsAvailableInRoutes()) return false; 274 return true; 275 } 276 277 private void notifyTurnoutLockObjectOfNewAlignment(int requestedState) { 278 if (_mTurnoutLock != null) _mTurnoutLock.dispatcherCommandedState(requestedState); 279 } 280 281// If it doesn't exist, this returns OUTOFCORRESPONDENCE, else return it's present state: 282// NOTE: IF a preconditioned input was available, it OVERRIDES actual Switch Direction Lever (which is ignored in this case). 283 private int getSwitchDirectionLeverRequestedState(boolean allowMergeInPreconditioning) { 284 if (_mSwitchDirectionLever == null) return CTCConstants.OUTOFCORRESPONDENCE; 285 if (allowMergeInPreconditioning && _mPreconditioningData._mCodeButtonPressed) { // We can check and it is available: 286 if (_mPreconditioningData._mSwitchDirectionLeverWas == CTCConstants.SWITCHNORMAL 287 || _mPreconditioningData._mSwitchDirectionLeverWas == CTCConstants.SWITCHREVERSED) { // Was valid: 288 return _mPreconditioningData._mSwitchDirectionLeverWas; 289 } 290 } 291 return _mSwitchDirectionLever.getPresentState(); 292 } 293 294// If it doesn't exist, this returns true. 295 private boolean switchDirectionIndicatorsInCorrespondence() { 296 if (_mSwitchDirectionIndicators != null) return _mSwitchDirectionIndicators.inCorrespondence(); 297 return true; 298 } 299 300 private void possiblyAllowSignalDirectionChange() { 301 if (allowSignalDirectionChangePart1()) { 302 int presentSignalDirectionLever = getCurrentSignalDirectionLever(true); 303 int presentSignalDirectionIndicatorsDirection = _mSignalDirectionIndicators.getPresentDirection(); // Object always exists! 304 boolean requestedChangeInSignalDirection = (presentSignalDirectionLever != presentSignalDirectionIndicatorsDirection); 305// If Dispatcher is asking for a cleared signal direction: 306 if (presentSignalDirectionLever != CTCConstants.SIGNALSNORMAL) { 307 if (!requestedChangeInSignalDirection) return; // If presentSignalDirectionLever is the same as the current state, DO NOTHING! 308 } 309// If user is trying to change direction, FORCE to "SIGNALSNORMAL" per Rick Moser response of 6/29/16: 310 if (presentSignalDirectionLever == CTCConstants.LEFTTRAFFIC && presentSignalDirectionIndicatorsDirection == CTCConstants.RIGHTTRAFFIC) 311 presentSignalDirectionLever = CTCConstants.SIGNALSNORMAL; 312 else if (presentSignalDirectionLever == CTCConstants.RIGHTTRAFFIC && presentSignalDirectionIndicatorsDirection == CTCConstants.LEFTTRAFFIC) 313 presentSignalDirectionLever = CTCConstants.SIGNALSNORMAL; 314 315 if (allowSignalDirectionChangePart2(presentSignalDirectionLever)) { 316// Tell SignalDirectionIndicators what the current requested state is: 317 _mSignalDirectionIndicators.setPresentSignalDirectionLever(presentSignalDirectionLever); 318 _mSignalDirectionIndicators.codeButtonPressed(presentSignalDirectionLever, requestedChangeInSignalDirection); 319 } 320 } 321 } 322 323 private boolean allowSignalDirectionChangePart1() { 324// Safety Checks: 325 if (_mSignalDirectionLever == null) return false; 326// Rules: 327// 6/28/16: If the signal direction indicators are presently "OUTOFCORRESPONDENCE", IGNORE request, as we are presently working on a change: 328 if (!_mSignalDirectionIndicators.inCorrespondence()) return false; 329 if (!turnoutPresentlyLocked()) return false; 330 return true; // Allowed "so far". 331 } 332 333 private boolean allowSignalDirectionChangePart2(int presentSignalDirectionLever) { 334// Safety Checks: (none so far) 335// Rules: 336 if (presentSignalDirectionLever != CTCConstants.SIGNALSNORMAL) { 337// If asking for a route and these indicates an error (a conflict), DO NOTHING! 338 if (!trafficLockingValid(presentSignalDirectionLever)) return false; // Do NOTHING at this time! 339 } 340 return true; // Allowed 341 } 342 343 private boolean trafficLockingValid(int presentSignalDirectionLever) { 344// If asking for a route and it indicates an error (a conflict), DO NOTHING! 345 if (_mTrafficLocking != null) { 346 TrafficLockingInfo trafficLockingInfo = _mTrafficLocking.valid(presentSignalDirectionLever, _mFleeting.isFleetingEnabled()); 347 _mLockedRoute = trafficLockingInfo._mLockedRoute; // Can be null! This is the bread crumb trail when running time expires. 348 return trafficLockingInfo._mReturnStatus; 349 } 350 return true; // Valid 351 } 352 353 private void possiblyAllowLockChange() { 354 if (allowLockChange()) _mTurnoutLock.codeButtonPressed(); 355 } 356 357 private boolean allowLockChange() { 358// Safety checks: 359 if (_mTurnoutLock == null) return false; 360// Rules: 361// Degenerate case: If we ONLY have a lock toggle switch, code button and lock indicator then: 362// if these 3 are null and the provided signalDirectionIndocatorsObject is non functional, therefore ALWAYS allow it! 363// if (_mSignalDirectionIndicators.isNonfunctionalObject() && _mSignalDirectionLever == null && _mSwitchDirectionIndicators == null && _mSwitchDirectionLever == null) return true; 364// If this is a normal O.S. section, then if either is occupied, DO NOT allow unlock. 365// If this is NOT an O.S. section, but only a lock, AND the dispatcher is trying 366// to UNLOCK or LOCK this section, occupancy is not considered: 367 if (!_mTurnoutLockingOnlyEnabled) { // Normal O.S. section: 368 if (isEitherOSSectionOccupied()) return false; 369 } 370 if (!_mTurnoutLock.tryingToChangeLockStatus()) return false; 371 if (routeClearedAcross()) return false; 372 if (!_mSignalDirectionIndicators.signalsNormal()) return false; 373 if (!switchDirectionIndicatorsInCorrespondence()) return false; 374 if (!areOSSensorsAvailableInRoutes()) return false; 375 return true; 376 } 377 378 private boolean routeClearedAcross() { 379 if (_mIndicationLockingSignals != null) return _mIndicationLockingSignals.routeClearedAcross(); 380 return false; // Default: Nothing to evaluate, nothing cleared thru! 381 } 382 383 private boolean turnoutPresentlyLocked() { 384 if (_mTurnoutLock == null) return true; // Doesn't exist, assume locked so that anything can be done to it. 385 return _mTurnoutLock.turnoutPresentlyLocked(); 386 } 387 388// For "isEitherOSSectionOccupied" and "isPrimaryOSSectionOccupied" below, 389// INCONSISTENT, UNKNOWN and OCCUPIED are all considered OCCUPIED(ACTIVE). 390 private boolean isEitherOSSectionOccupied() { 391 return _mOSSectionOccupiedExternalSensor.getKnownState() != Sensor.INACTIVE || _mOSSectionOccupiedExternalSensor2.getKnownState() != Sensor.INACTIVE; 392 } 393 394// See "isEitherOSSectionOccupied" comment. 395 private boolean isPrimaryOSSectionOccupied() { 396 return _mOSSectionOccupiedExternalSensor.getKnownState() != Sensor.INACTIVE; 397 } 398 399 private boolean areOSSensorsAvailableInRoutes() { 400 HashSet<Sensor> sensors = new HashSet<>(); 401 sensors.add(_mOSSectionOccupiedExternalSensor.getBean()); 402 if (_mOSSectionOccupiedExternalSensor2.valid()) sensors.add(_mOSSectionOccupiedExternalSensor2.getBean()); 403 return _mLockedRoutesManager.checkRoute(sensors, _mUserIdentifier, "Turnout Check"); 404 } 405}