001package jmri.jmrit.ussctc; 002 003import java.util.*; 004import jmri.*; 005 006/** 007 * Drive a single Turnout section on a USS CTC panel. 008 * Implements {@link Section} for both the field and CTC machine parts. 009 * <a href="doc-files/TurnoutSection-ClassDiagram.png"><img src="doc-files/TurnoutSection-ClassDiagram.png" alt="UML Class diagram" height="50%" width="50%"></a> 010 * The two parts 011 * are implemented as separate {@link FieldSection} and {@link CentralSection} 012 * static inner classes to ensure they're functionally separate, connected only 013 * by the code they exchange. They're combined in this single class 014 * to make sure they work together. 015 * <p> 016 * Note that this intentionally does not turn off indicators when the code button 017 * is pressed unless a change has been requested. This is a model-railroad compromise 018 * to speed up the dispatcher's ability to see what's going on. 019 * <p> 020 * The state diagram for the central section is presented in three parts to make it more useful: 021 *<ul> 022 * <li>Initialization 023 * <a href="doc-files/TurnoutSection-Central-Init-StateDiagram.png"><img src="doc-files/TurnoutSection-Central-Init-StateDiagram.png" alt="UML State diagram" height="33%" width="33%"></a> 024 * <li>Handline code button presses 025 * <a href="doc-files/TurnoutSection-Central-Code-StateDiagram.png"><img src="doc-files/TurnoutSection-Central-Code-StateDiagram.png" alt="UML State diagram" height="33%" width="33%"></a> 026 * <li>Receiving indications 027 * <a href="doc-files/TurnoutSection-Central-Indication-StateDiagram.png"><img src="doc-files/TurnoutSection-Central-Indication-StateDiagram.png" alt="UML State diagram" height="33%" width="33%"></a> 028 * </ul> 029 * @author Bob Jacobsen Copyright (C) 2007, 2017 030 * TODO - add field state diagram 031 */ 032/* 033 * @startuml jmri/jmrit/ussctc/doc-files/TurnoutSection-ClassDiagram.png 034 * FieldSection <|-- Section 035 * CentralSection <|-- Section 036 * Section <|-- TurnoutSection 037 * FieldSection <|-- TurnoutFieldSection 038 * CentralSection <|-- TurnoutCentralSection 039 * TurnoutSection *.. TurnoutCentralSection 040 * TurnoutSection *.. TurnoutFieldSection 041 * 'note A TurnoutSection object comprises itself, plus\ncontained CentralSection and FieldSection objects 042 @end 043 */ 044/* 045 * @startuml jmri/jmrit/ussctc/doc-files/TurnoutSection-Central-Init-StateDiagram.png 046 * state "Showing 10 Normal" as ShowN 047 * state "Showing 01 Reversed" as ShowR 048 * state "Showing 00 Off" as ShowOff 049 * 050 * note bottom of ShowOff : At startup, indicator lights match layout turnout state 051 * 052 * [*] --> ShowN : CLOSED at startup 053 * [*] --> ShowR : THROWN at startup 054 * [*] --> ShowOff : Unknown at startup 055 @end 056 */ 057/* 058 * @startuml jmri/jmrit/ussctc/doc-files/TurnoutSection-Central-Code-StateDiagram.png 059 * state "Showing 10 Normal" as ShowN 060 * state "Showing 01 Reversed" as ShowR 061 * state "Showing 00 Off" as ShowOff 062 * 063 * note bottom of ShowOff : Pressing code results in lights off\nif lamps and lever don't match 064 * 065 * ShowR --> ShowOff : Lever at Normal\nand Code pressed 066 * ShowR --> ShowR : Lever at Reversed\nand Code pressed 067 * ShowN --> ShowN: Lever at Normal\nand Code pressed 068 * ShowN --> ShowOff : Lever at Reversed\nand Code pressed 069 @end 070 */ 071/* 072 * @startuml jmri/jmrit/ussctc/doc-files/TurnoutSection-Central-Indication-StateDiagram.png 073 * state "Showing 10 Normal" as ShowN 074 * state "Showing 01 Reversed" as ShowR 075 * state "Showing 00 Off" as ShowOff 076 * 077 * ShowOff --> ShowN : Indication 10 received 078 * ShowN --> ShowN : Indication 10 received 079 * ShowR --> ShowN : Indication 10 received 080 * 081 * ShowOff --> ShowR : Indication 01 received 082 * ShowN --> ShowR : Indication 01 received 083 * ShowR --> ShowR : Indication 01 received 084 * 085 * ShowOff --> ShowOff: Indication 00 received 086 * ShowR --> ShowOff: Indication 00 received 087 * ShowN --> ShowOff: Indication 00 received 088 * 089 * note left of ShowOff : Indications always drive the display 090 @end 091 */ 092public class TurnoutSection implements Section<CodeGroupTwoBits, CodeGroupTwoBits> { 093 094 /** 095 * Anonymous object only for testing 096 */ 097 TurnoutSection() {} 098 099 TurnoutFieldSection field; 100 TurnoutCentralSection central; 101 102 /** 103 * Create and configure. 104 * 105 * Accepts user or system names. 106 * 107 * @param layoutTO Name for turnout on railroad 108 * @param normalIndicator Turnout name for normal (left) indicator light on panel 109 * @param reversedIndicator Turnout name for reversed (right) indicator light on panel 110 * @param normalInput Sensor name for normal (left) side of switch on panel 111 * @param reversedInput Sensor name for reversed (right) side of switch on panel 112 * @param station Station to which this Section belongs 113 */ 114 public TurnoutSection(String layoutTO, String normalIndicator, String reversedIndicator, String normalInput, String reversedInput, Station<CodeGroupTwoBits, CodeGroupTwoBits> station) { 115 TurnoutManager tm = InstanceManager.getDefault(TurnoutManager.class); 116 this.station = station; 117 118 central = new TurnoutCentralSection(normalIndicator, reversedIndicator, normalInput, reversedInput); 119 120 field = new TurnoutFieldSection(layoutTO); 121 122 central.initializeLamps(tm.provideTurnout(layoutTO)); 123 field.initializeState(tm.provideTurnout(layoutTO)); 124 } 125 126 public void addLocks(List<Lock> locks) { 127 field.addLocks(locks); 128 } 129 130 Station<CodeGroupTwoBits, CodeGroupTwoBits> station; 131 @Override 132 public Station<CodeGroupTwoBits, CodeGroupTwoBits> getStation() { return station; } 133 @Override 134 public String getName() { return "TO for "+field.hLayoutTO.getBean().getDisplayName(); } 135 136 // coding used locally to ensure consistency 137 static final CodeGroupTwoBits CODE_CLOSED = CodeGroupTwoBits.Double10; 138 static final CodeGroupTwoBits CODE_THROWN = CodeGroupTwoBits.Double01; 139 static final CodeGroupTwoBits CODE_NEITHER = CodeGroupTwoBits.Double00; 140 141 @Override 142 public CodeGroupTwoBits codeSendStart() { return central.codeSendStart(); } 143 @Override 144 public void codeValueDelivered(CodeGroupTwoBits value) { field.codeValueDelivered(value); } 145 @Override 146 public CodeGroupTwoBits indicationStart() { return field.indicationStart(); } 147 @Override 148 public void indicationComplete(CodeGroupTwoBits value) { central.indicationComplete(value); } 149 150 @Override 151 public String toString() { 152 String retval; 153 154 retval = getName() 155 +" central: "+central.state 156 +" field lastCode: "+field.lastCodeValue+" lastInd "+field.lastIndicationValue; 157 158 return retval; 159 } 160 161 static class TurnoutCentralSection implements CentralSection<CodeGroupTwoBits, CodeGroupTwoBits> { 162 public TurnoutCentralSection(String normalIndicator, String reversedIndicator, String normalInput, String reversedInput) { 163 NamedBeanHandleManager hm = InstanceManager.getDefault(NamedBeanHandleManager.class); 164 TurnoutManager tm = InstanceManager.getDefault(TurnoutManager.class); 165 SensorManager sm = InstanceManager.getDefault(SensorManager.class); 166 167 hNormalIndicator = hm.getNamedBeanHandle(normalIndicator, tm.provideTurnout(normalIndicator)); 168 hReversedIndicator = hm.getNamedBeanHandle(reversedIndicator, tm.provideTurnout(reversedIndicator)); 169 170 hNormalInput = hm.getNamedBeanHandle(normalInput, sm.provideSensor(normalInput)); 171 hReversedInput = hm.getNamedBeanHandle(reversedInput, sm.provideSensor(reversedInput)); 172 } 173 174 State state = State.DARK_INCONSISTENT; 175 176 NamedBeanHandle<Turnout> hNormalIndicator; 177 NamedBeanHandle<Turnout> hReversedIndicator; 178 179 NamedBeanHandle<Sensor> hNormalInput; 180 NamedBeanHandle<Sensor> hReversedInput; 181 182 enum State { 183 SHOWING_NORMAL, 184 SHOWING_REVERSED, 185 /** 186 * Command has gone to layout, no verification of move has come back yet; both indicators OFF 187 */ 188 DARK_WAITING_REPLY, 189 /** 190 * A lock has forbidden move or turnout inconsistent; both indicators OFF 191 */ 192 DARK_INCONSISTENT 193 } 194 195 void initializeLamps(Turnout to) { 196 // initialize lamps to follow layout state 197 if (to.getKnownState()==Turnout.THROWN) { 198 hNormalIndicator.getBean().setCommandedState(Turnout.CLOSED); 199 hReversedIndicator.getBean().setCommandedState(Turnout.THROWN); 200 state = State.SHOWING_REVERSED; 201 } else if (to.getKnownState()==Turnout.CLOSED) { 202 hNormalIndicator.getBean().setCommandedState(Turnout.THROWN); 203 hReversedIndicator.getBean().setCommandedState(Turnout.CLOSED); 204 state = State.SHOWING_NORMAL; 205 } else { 206 hNormalIndicator.getBean().setCommandedState(Turnout.CLOSED); 207 hReversedIndicator.getBean().setCommandedState(Turnout.CLOSED); 208 state = State.DARK_INCONSISTENT; 209 } 210 } 211 212 /** 213 * Start of sending code operation: 214 * <ul> 215 * <li>Set indicators 216 * <li>Provide values to send over line 217 * </ul> 218 * @return code line value to transmit 219 */ 220 @Override 221 public CodeGroupTwoBits codeSendStart() { 222 // Set the indicators based on current and requested state 223 if ( (state == State.SHOWING_NORMAL && hNormalInput.getBean().getKnownState()==Sensor.ACTIVE) 224 || (state == State.SHOWING_REVERSED && hReversedInput.getBean().getKnownState()==Sensor.ACTIVE) ) { 225 log.debug("No turnout change requested, lamps left on"); 226 } else { 227 log.debug("Turnout change requested, turn lamps off"); 228 // have to turn off 229 hNormalIndicator.getBean().setCommandedState(Turnout.CLOSED); 230 hReversedIndicator.getBean().setCommandedState(Turnout.CLOSED); 231 state = State.DARK_WAITING_REPLY; 232 } 233 234 // return the settings to send 235 if (hNormalInput.getBean().getKnownState()==Sensor.ACTIVE) return CODE_CLOSED; 236 if (hReversedInput.getBean().getKnownState()==Sensor.ACTIVE) return CODE_THROWN; 237 return CODE_NEITHER; 238 } 239 240 /** 241 * Process values received from the field unit. 242 */ 243 @Override 244 public void indicationComplete(CodeGroupTwoBits value) { 245 log.debug("Indication sets from {}", value); 246 if (value == CODE_CLOSED) { 247 hNormalIndicator.getBean().setCommandedState(Turnout.THROWN); 248 hReversedIndicator.getBean().setCommandedState(Turnout.CLOSED); 249 state = State.SHOWING_NORMAL; 250 } else if (value == CODE_THROWN) { 251 hNormalIndicator.getBean().setCommandedState(Turnout.CLOSED); 252 hReversedIndicator.getBean().setCommandedState(Turnout.THROWN); 253 state = State.SHOWING_REVERSED; 254 } else if (value == CODE_NEITHER) { 255 hNormalIndicator.getBean().setCommandedState(Turnout.CLOSED); 256 hReversedIndicator.getBean().setCommandedState(Turnout.CLOSED); 257 state = State.DARK_INCONSISTENT; 258 } else log.error("Got code not recognized: {}", value); 259 } 260 } 261 262 class TurnoutFieldSection implements FieldSection<CodeGroupTwoBits, CodeGroupTwoBits> { 263 264 /** 265 * Defines intended (commanded by central) field for this state. 266 */ 267 CodeGroupTwoBits lastCodeValue = CODE_NEITHER; 268 269 /** 270 * Last indication actually sent 271 */ 272 CodeGroupTwoBits lastIndicationValue = CODE_NEITHER; 273 274 public TurnoutFieldSection(String layoutTO) { 275 NamedBeanHandleManager hm = InstanceManager.getDefault(NamedBeanHandleManager.class); 276 TurnoutManager tm = InstanceManager.getDefault(TurnoutManager.class); 277 278 hLayoutTO = hm.getNamedBeanHandle(layoutTO, tm.provideTurnout(layoutTO)); 279 280 tm.provideTurnout(layoutTO).addPropertyChangeListener((java.beans.PropertyChangeEvent e) -> {layoutTurnoutChanged(e);}); 281 } 282 283 NamedBeanHandle<Turnout> hLayoutTO; 284 285 List<Lock> locks; 286 public void addLocks(List<Lock> locks) { this.locks = locks; } 287 288 /** 289 * Initially, align with what's in the field 290 * @param to Turnout in field to align to 291 */ 292 void initializeState(Turnout to) { 293 if (to.getCommandedState() == Turnout.CLOSED) { 294 lastCodeValue = CODE_CLOSED; 295 } else if (to.getCommandedState() == Turnout.THROWN) { 296 lastCodeValue = CODE_THROWN; 297 } else { 298 lastCodeValue = CODE_NEITHER; 299 } 300 301 lastIndicationValue = lastCodeValue; 302 } 303 304 /** 305 * Notification that code has arrived in the field. Sets the turnout on the layout. 306 */ 307 @Override 308 public void codeValueDelivered(CodeGroupTwoBits value) { 309 lastCodeValue = value; 310 311 // Set turnout as commanded, skipping redundant operations 312 if (value == CODE_CLOSED && hLayoutTO.getBean().getCommandedState() != Turnout.CLOSED) { 313 if (Lock.checkLocksClear(locks, Lock.turnoutLockLogger)) { 314 hLayoutTO.getBean().setCommandedState(Turnout.CLOSED); 315 log.debug("Layout turnout set CLOSED"); 316 } else logLocked(value); 317 } else if (value == CODE_THROWN && hLayoutTO.getBean().getCommandedState() != Turnout.THROWN) { 318 if (Lock.checkLocksClear(locks, Lock.turnoutLockLogger)) { 319 hLayoutTO.getBean().setCommandedState(Turnout.THROWN); 320 log.debug("Layout turnout set THROWN"); 321 } else logLocked(value); 322 } else { 323 log.debug("Layout turnout already set for {} as {}", value, hLayoutTO.getBean().getCommandedState()); 324 // Usually, indication will come back when turnout feedback (defined elsewhere) triggers 325 // from motion run above 326 // But we have to handle the case of e.g. re-commanding back to the current turnout state 327 if ( lastIndicationValue != getCurrentIndication() ) { 328 329 log.debug(" Last indication {} doesn't match current {}, request indication", lastIndicationValue, getCurrentIndication()); 330 jmri.util.ThreadingUtil.runOnLayoutEventually( ()->{ station.requestIndicationStart(); } ); 331 332 } 333 } 334 } 335 336 void logLocked(CodeGroupTwoBits value) { 337 log.debug("No turnout operation due to not permitted by lock: {}", value); 338 // Usually, indication will come back when turnout feedback (defined elsewhere) triggers 339 // from motion run above 340 // But we have to handle the case of re-commanding back to the current turnout state 341 if ( lastIndicationValue != getCurrentIndication() ) { 342 343 log.debug(" Locked, but last indication {} doesn't match current {}, request indication", lastIndicationValue, getCurrentIndication()); 344 jmri.util.ThreadingUtil.runOnLayoutEventually( ()->{ station.requestIndicationStart(); } ); 345 346 } 347 } 348 /** 349 * Provide state that's returned from field to machine via indication. 350 */ 351 @Override 352 public CodeGroupTwoBits indicationStart() { 353 lastIndicationValue = getCurrentIndication(); 354 return lastIndicationValue; 355 } 356 357 public CodeGroupTwoBits getCurrentIndication() { 358 if (hLayoutTO.getBean().getKnownState() == Turnout.CLOSED && lastCodeValue == CODE_CLOSED ) { 359 return CODE_CLOSED; 360 } else if (hLayoutTO.getBean().getKnownState() == Turnout.THROWN && lastCodeValue == CODE_THROWN) { 361 return CODE_THROWN; 362 } else { 363 return CODE_NEITHER; 364 } 365 } 366 367 void layoutTurnoutChanged(java.beans.PropertyChangeEvent e) { 368 if (e.getPropertyName().equals("KnownState") && !e.getNewValue().equals(e.getOldValue()) ) { 369 log.debug("Turnout changed from {} to {}, so requestIndicationStart", e.getOldValue(), e.getNewValue()); 370 // Always send an indication if there's a change in the turnout 371 station.requestIndicationStart(); 372 } 373 } 374 } 375 376 private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(TurnoutSection.class); 377}