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}