001/* 002 * @author Gregory J. Bedlek Copyright (C) 2018, 2019 003 * 004 */ 005package jmri.jmrit.ctc; 006 007import java.util.ArrayList; 008import java.util.HashSet; 009import jmri.Block; 010import jmri.NamedBeanHandle; 011import jmri.Sensor; 012import jmri.jmrit.ctc.ctcserialdata.CallOnData; 013import jmri.jmrit.ctc.ctcserialdata.OtherData; 014 015/* 016This module supports Call On functionality. 017 018SignalHeads: Clears the "held" bit and sets the signal to whatever the user 019specified. CalledOnSensor can be "" if you want the Dispatcher to call on to 020an unoccupied block, or the real sensor for the block being called on to. 021 022SignalMasts: Clears the "held" bit, and set the permissive value in the called 023on block. In addition, see the block comment above where this is done regarding 024the bug in JMRI we fix. When the O.S. section becomes occupied, we clear the 025permissive value in the called on block. 026 027Both: 028If the occupancy sensor is specified and that sensor is INACTIVE (indicating 029nothing in block), then the call on is IGNORED. 030Resets the "callOnToggleSensor" to INACTIVE, thereby turning off the 031Call On request by the dispatcher. 032 033Issues with a computer vs. a real CTC machine: 034 035If you have a call on button AND you set it to momentary and you have a 036computer monitor (vs. a physical dispatchers panel): 037It is IMPOSSIBLE to press both the Call on button AND the code button at the 038same time. You could make the call on button NON momentary, but from my experiment 039(please perform you own), the physical indication of the button pressed is not 040good, and if you press it again, it toggles off which is not what you want. 041I suggest using a toggle switch. This object when configured for using it 042will "reset" the toggle to off once the code button has been pressed 043thus providing a better indication that it occurred. In the future, if you 044substitute a push button and forget to set the "momentary" attribute, this 045routine will still reset it for the next use. 046 047Rules from http://www.ctcparts.com/about.htm 048 049"An important note though for programming logic is that the interlocking limits 050must be clear and all power switches within the interlocking limits aligned 051appropriately for the back to train route for this feature to activate." 052 053I was either told or read somewhere that the block being called on to MUST be 054occupied for the call on to work, otherwise the CODE BUTTON PRESS is IGNORE! 055 056By the way, there is NO way to do flashing any color with a semaphore! 057You should probably use "YELLOW" in that case! 058 059*/ 060public class CallOn { 061 private static class GroupingData { 062 public final NBHSignal _mSignal; // Signal 063 public final int _mSignalHeadFaces; // Which way above faces. 064 public final int _mCallOnAspect; // What it should be set to if CallOn sucessful. 065 public final NBHSensor _mCalledOnExternalSensor; 066 public final NamedBeanHandle<Block> _mNamedBeanHandleBlock; 067 public final SwitchIndicatorsRoute _mRoute; 068// NOTE: When calling this constructor, ALL values MUST BE VALID! NO check is done! 069 public GroupingData(NBHSignal signal, String signalHeadFaces, int callOnAspect, NBHSensor calledOnExternalSensor, NamedBeanHandle<Block> namedBeanHandleBlock, SwitchIndicatorsRoute route) { 070 _mSignal = signal; 071 _mSignalHeadFaces = signalHeadFaces.equals(Bundle.getMessage("InfoDlgCOLeftTraffic")) ? CTCConstants.LEFTTRAFFIC : CTCConstants.RIGHTTRAFFIC; // NOI18N 072 _mCallOnAspect = callOnAspect; 073 _mCalledOnExternalSensor = calledOnExternalSensor; 074 _mNamedBeanHandleBlock = namedBeanHandleBlock; 075 _mRoute = route; 076 } 077 } 078 079 private final LockedRoutesManager _mLockedRoutesManager; 080 private final boolean _mSignalHeadSelected; 081 private final NBHSensor _mCallOnToggleSensor; 082 private final ArrayList<GroupingData> _mGroupingDataArrayList = new ArrayList<>(); 083 084 public CallOn(LockedRoutesManager lockedRoutesManager, String userIdentifier, NBHSensor callOnToggleSensor, ArrayList<CallOnData> groupingsList, OtherData.SIGNAL_SYSTEM_TYPE signalSystemType) { 085 _mLockedRoutesManager = lockedRoutesManager; 086 _mSignalHeadSelected = (signalSystemType == OtherData.SIGNAL_SYSTEM_TYPE.SIGNALHEAD); 087 _mCallOnToggleSensor = callOnToggleSensor; 088 for (CallOnData callOnData : groupingsList) { 089 try { 090 NBHSignal signal = callOnData._mExternalSignal; 091 String trafficDirection = callOnData._mSignalFacingDirection; 092 if (!trafficDirection.equals(Bundle.getMessage("InfoDlgCOLeftTraffic")) && !trafficDirection.equals(Bundle.getMessage("InfoDlgCORightTraffic"))) { // NOI18N 093 throw new CTCException("CallOn", userIdentifier, "groupingString", callOnData.toString() + " not " + Bundle.getMessage("InfoDlgCOLeftTraffic") + " or " + Bundle.getMessage("InfoDlgCORightTraffic") + "."); // NOI18N 094 } 095 SwitchIndicatorsRoute route = new SwitchIndicatorsRoute(callOnData._mSwitchIndicators); 096 if (_mSignalHeadSelected) { 097// Technically, I'd have liked to call this only once, but in reality, each signalhead could have a different value list: 098 String[] validStateNames = signal.getValidStateNames(); // TODO consider using getValidStateKeys() to skip localisation issue 099 int validStateNamesIndex = arrayFind(validStateNames, convertFromForeignLanguageColor(callOnData._mSignalAspectToDisplay)); 100 // TODO use non-localized validStateNKeys instead of localized validStateNames 101 if (validStateNamesIndex == -1) { // Not found: 102 throw new CTCException("CallOn", userIdentifier, "groupingString", callOnData.toString() + " " + Bundle.getMessage("CallOnNotValidAspect")); // NOI18N 103 } 104 NBHSensor calledOnExternalSensor = callOnData._mCalledOnExternalSensor; 105 int[] correspondingValidStates = signal.getValidStates(); // I ASSUME it's a correlated 1 for 1 with "getValidStateNames", via tests it seems to be. 106 _mGroupingDataArrayList.add(new GroupingData(signal, trafficDirection, correspondingValidStates[validStateNamesIndex], calledOnExternalSensor, null, route)); 107 } else { 108 NamedBeanHandle<Block> externalBlock = callOnData._mExternalBlock; 109 _mGroupingDataArrayList.add(new GroupingData(signal, trafficDirection, 0, null, externalBlock, route)); 110 } 111 } catch (CTCException e) { e.logError(); return; } 112 } 113 resetToggle(); 114 } 115 116 public void removeAllListeners() {} // None done. 117 118 public void resetToggle() { 119 _mCallOnToggleSensor.setKnownState(Sensor.INACTIVE); 120 } 121 122/* Call On requested. CodeButtonHandler has determined from it's "limited" 123 viewpoint that it is OK to attempt the call on. This routine determines 124 if it is fully valid to allow it at this time. 125 126NOTE: 127 We "fake out" the caller: we return "true" AS IF we actually did the 128 call on. Why? So higher level code in CodeButtonHandler DOES NOT attempt 129 to try normal signal direction lever handling! When the dispatcher requests 130 call on, normal signal direction lever handling should be bypassed. If we 131 didn't, then when the dispatcher requested call on to an unoccupied block, 132 the signals would go to yellow/green instead of staying at signals normal 133 (all stop) which would be the result of the call on only, which is what we 134 want. After all, that's what the dispatcher asked for explicitly! 135*/ 136 public TrafficLockingInfo codeButtonPressed(HashSet<Sensor> sensors, 137 String userIdentifier, 138 SignalDirectionIndicatorsInterface signalDirectionIndicatorsObject, 139 int signalDirectionLever) { 140 if (_mCallOnToggleSensor.getKnownState() == Sensor.INACTIVE) return new TrafficLockingInfo(false); // Dispatcher didn't want it at this time! 141 if (signalDirectionLever != CTCConstants.LEFTTRAFFIC && signalDirectionLever != CTCConstants.RIGHTTRAFFIC) return new TrafficLockingInfo(false); // Doesn't make sense, don't do anything 142 143 GroupingData foundGroupingData = null; 144 145 int ruleNumber = 0; 146 for (GroupingData groupingData : _mGroupingDataArrayList) { 147 ruleNumber++; 148 if (groupingData._mSignalHeadFaces == signalDirectionLever) { 149 if (groupingData._mRoute.isRouteSelected()) { 150 foundGroupingData = groupingData; 151 break; 152 } 153 } 154 } 155// From NOW ON, the "returnValue" status will be true: 156 TrafficLockingInfo returnValue = new TrafficLockingInfo(true); 157 if (foundGroupingData == null) return returnValue; // Has to be active, pretend we did it, but we didn't! 158 159 if (_mSignalHeadSelected) { 160 if (Sensor.ACTIVE != foundGroupingData._mCalledOnExternalSensor.getKnownState()) return returnValue; // Has to be active EXACTLY, pretend we did it, but we didn't! 161 if (foundGroupingData._mCalledOnExternalSensor.valid()) { 162 sensors.add(foundGroupingData._mCalledOnExternalSensor.getBean()); 163 } 164// Check to see if the route specified is free: 165// The route is the O.S. section that called us, along with the called on occupancy sensor: 166 returnValue._mLockedRoute = _mLockedRoutesManager.checkRouteAndAllocateIfAvailable(sensors, userIdentifier, "Rule #" + ruleNumber, signalDirectionLever == CTCConstants.RIGHTTRAFFIC); 167 if (returnValue._mLockedRoute == null) return returnValue; // Not available, fake out 168 169 foundGroupingData._mSignal.setHeld(false); // Original order in .py code 170 foundGroupingData._mSignal.setAppearance(foundGroupingData._mCallOnAspect); 171 } else { // Signal mast 172// We get this EVERY time (we don't cache this in "foundGroupingData._mCalledOnExternalSensor") because this property of Block 173// can be changed DYNAMICALLY at runtime via the Block Editor: 174 NBHSensor sensor = new NBHSensor(foundGroupingData._mNamedBeanHandleBlock.getBean().getNamedSensor()); 175 if (Sensor.ACTIVE != sensor.getKnownState()) return returnValue; // Has to be active EXACTLY, pretend we did it, but we didn't! 176 if (sensor.valid()) { 177 sensors.add(sensor.getBean()); 178 } 179 180// Check to see if the route specified is free: 181// The route is the O.S. section that called us, along with the called on occupancy sensor: 182 returnValue._mLockedRoute = _mLockedRoutesManager.checkRouteAndAllocateIfAvailable(sensors, userIdentifier, "Rule #" + ruleNumber, signalDirectionLever == CTCConstants.RIGHTTRAFFIC); 183 if (returnValue._mLockedRoute == null) return returnValue; // Not available, fake out 184 foundGroupingData._mSignal.allowPermissiveSML(); 185 foundGroupingData._mSignal.setHeld(false); 186 } 187 188 signalDirectionIndicatorsObject.setRequestedDirection(signalDirectionLever); 189// These two statements MUST be last thing in this order: 190 signalDirectionIndicatorsObject.setSignalDirectionIndicatorsToOUTOFCORRESPONDENCE(); 191 signalDirectionIndicatorsObject.startCodingTime(); 192 return returnValue; 193 } 194 195 static private int arrayFind(String[] array, String aString) { 196 for (int index = 0; index < array.length; index++) { 197 if (aString.equals(array[index])) return index; 198 } 199 return -1; 200 } 201 202// When we went to foreign language support, I had to convert to English here, so that these lines worked above: 203// String[] validStateNames = signal.getValidStateNames(); // use getValidStateKeys instead? 204// int validStateNamesIndex = arrayFind(validStateNames, convertFromForeignLanguageColor(callOnEntry._mSignalAspectToDisplay)); 205// 206// I SUSPECT (not verified) that "signal.getValidStateNames()" ALWAYS returns English no matter what language is selected. 207// If I AM WRONG, then this routine can be removed, and the call to it removed: 208 private String convertFromForeignLanguageColor(String foreignLanguageColor) { 209 String color = "Red"; // should NEVER be used directly, but if programmers screw up, default to some "sane" value. 210 if (foreignLanguageColor.equals(Bundle.getMessage("SignalHeadStateDark"))) color = "Dark"; // NOI18N 211 if (foreignLanguageColor.equals(Bundle.getMessage("SignalHeadStateRed"))) color = "Red"; // NOI18N 212 if (foreignLanguageColor.equals(Bundle.getMessage("SignalHeadStateYellow"))) color = "Yellow"; // NOI18N 213 if (foreignLanguageColor.equals(Bundle.getMessage("SignalHeadStateGreen"))) color = "Green"; // NOI18N 214 if (foreignLanguageColor.equals(Bundle.getMessage("SignalHeadStateFlashingRed"))) color = "Flashing Red"; // NOI18N 215 if (foreignLanguageColor.equals(Bundle.getMessage("SignalHeadStateFlashingYellow"))) color = "Flashing Yellow"; // NOI18N 216 if (foreignLanguageColor.equals(Bundle.getMessage("SignalHeadStateFlashingGreen"))) color = "Flashing Green"; // NOI18N 217 if (foreignLanguageColor.equals(Bundle.getMessage("SignalHeadStateLunar"))) color = "Lunar"; // NOI18N 218 if (foreignLanguageColor.equals(Bundle.getMessage("SignalHeadStateFlashingLunar"))) color = "Flashing Lunar"; // NOI18N 219 return color; 220 } 221 222}