001package jmri.jmrit.ussctc;
002
003import java.beans.*;
004import java.util.*;
005import javax.annotation.OverridingMethodsMustInvokeSuper;
006import jmri.*;
007import jmri.util.*;
008
009/**
010 * Drive a signal section on a USS CTC panel.
011 * Implements {@link Section} for both the field and CTC machine parts.
012 * <p>
013 * Based on the Signal interface.
014 * <p>
015 * Note that this intentionally does not turn off indicators when the code button
016 * is pressed unless a change has been requested.  This is a model-railroad compromise
017 * to speed up the dispatcher's ability to see what's going on.
018 *
019 * @author Bob Jacobsen Copyright (C) 2007, 2017, 2021
020 */
021public class SignalHeadSection implements Section<CodeGroupThreeBits, CodeGroupThreeBits> {
022
023    /**
024     *  Anonymous object only for testing
025     */
026    SignalHeadSection() {
027        this.station = new Station<CodeGroupThreeBits, CodeGroupThreeBits>("1", null, new CodeButton("IS1","IT1"));
028    }
029
030    static final int DEFAULT_RUN_TIME_LENGTH = 30000;
031
032    /**
033     * Create and configure.
034     *
035     * Accepts user or system names.
036     *
037     * @param rightHeads  Set of Signals to release when rightward travel allowed
038     * @param leftHeads  Set of Signals to release when leftward travel allowed
039     * @param leftIndicator  Turnout name for leftward indicator
040     * @param stopIndicator  Turnout name for stop indicator
041     * @param rightIndicator  Turnout name for rightward indicator
042     * @param leftInput Sensor name for rightward side of lever on panel
043     * @param rightInput Sensor name for leftward side of lever on panel
044     * @param station Station to which this Section belongs
045     */
046    public SignalHeadSection(List<String> rightHeads, List<String> leftHeads,
047                             String leftIndicator, String stopIndicator, String rightIndicator,
048                             String leftInput, String rightInput,
049                             Station<CodeGroupThreeBits, CodeGroupThreeBits> station) {
050
051        this.station = station;
052
053        timeMemory = InstanceManager.getDefault(MemoryManager.class).getMemory(
054                        Constants.commonNamePrefix+"SIGNALHEADSECTION"+Constants.commonNameSuffix+"TIME"); // NOI18N
055        if (timeMemory == null) {
056            timeMemory = InstanceManager.getDefault(MemoryManager.class).provideMemory(
057                        Constants.commonNamePrefix+"SIGNALHEADSECTION"+Constants.commonNameSuffix+"TIME"); // NOI18N
058            timeMemory.setValue(Integer.valueOf(DEFAULT_RUN_TIME_LENGTH));
059        }
060
061        NamedBeanHandleManager hm = InstanceManager.getDefault(NamedBeanHandleManager.class);
062        TurnoutManager tm = InstanceManager.getDefault(TurnoutManager.class);
063        SensorManager sm = InstanceManager.getDefault(SensorManager.class);
064        SignalHeadManager shm = InstanceManager.getDefault(SignalHeadManager.class);
065
066        hRightHeads = new ArrayDeque<>();
067        for (String s : rightHeads) {
068            SignalHead sh = shm.getSignalHead(s);
069            if (sh != null) {
070                hRightHeads.add(hm.getNamedBeanHandle(s,sh));
071            } else {
072                log.debug("Signal {} for SignalHeadSection wasn't found", s); // NOI18N
073            }
074        }
075
076        hLeftHeads = new ArrayDeque<>();
077        for (String s : leftHeads) {
078            SignalHead sh = shm.getSignalHead(s);
079            if (sh != null) {
080                hLeftHeads.add(hm.getNamedBeanHandle(s,sh));
081            } else {
082                log.debug("Signal {} for SignalHeadSection wasn't found", s); // NOI18N
083            }
084        }
085
086        timeLogSensor = InstanceManager.getDefault(SensorManager.class).provideSensor("IS"+Constants.commonNamePrefix
087                                            +"SIGNALSECTION:"+station.getName()+":RUNNINGTIME"
088                                            +Constants.commonNameSuffix);
089        timeLogSensor.setCommandedState(Sensor.INACTIVE);
090
091        hLeftIndicator = hm.getNamedBeanHandle(leftIndicator, tm.provideTurnout(leftIndicator));
092        hStopIndicator = hm.getNamedBeanHandle(stopIndicator, tm.provideTurnout(stopIndicator));
093        hRightIndicator = hm.getNamedBeanHandle(rightIndicator, tm.provideTurnout(rightIndicator));
094
095        hLeftInput = hm.getNamedBeanHandle(leftInput, sm.provideSensor(leftInput));
096        hRightInput = hm.getNamedBeanHandle(rightInput, sm.provideSensor(rightInput));
097
098        // initialize lamps to follow layout state to STOP
099        tm.provideTurnout(leftIndicator).setCommandedState(Turnout.CLOSED);
100        tm.provideTurnout(stopIndicator).setCommandedState(Turnout.THROWN);
101        tm.provideTurnout(rightIndicator).setCommandedState(Turnout.CLOSED);
102        // hold everything
103        setListHeldState(hRightHeads, true);
104        setListHeldState(hLeftHeads, true);
105
106        // add listeners
107        for (NamedBeanHandle<Signal> b : hRightHeads) {
108            b.getBean().addPropertyChangeListener(
109                (java.beans.PropertyChangeEvent e) -> {
110                    jmri.util.ThreadingUtil.runOnLayoutEventually( ()->{
111                        layoutSignalHeadChanged(e);
112                    });
113                }
114            );
115        }
116        for (NamedBeanHandle<Signal> b : hLeftHeads) {
117            b.getBean().addPropertyChangeListener(
118                (java.beans.PropertyChangeEvent e) -> {
119                    jmri.util.ThreadingUtil.runOnLayoutEventually( ()->{
120                        layoutSignalHeadChanged(e);
121                    });
122                }
123            );
124        }
125    }
126
127    Memory timeMemory = null;
128
129    Sensor timeLogSensor;
130
131    ArrayDeque<NamedBeanHandle<Signal>> hRightHeads;
132    ArrayDeque<NamedBeanHandle<Signal>> hLeftHeads;
133
134    NamedBeanHandle<Turnout> hLeftIndicator;
135    NamedBeanHandle<Turnout> hStopIndicator;
136    NamedBeanHandle<Turnout> hRightIndicator;
137
138    NamedBeanHandle<Sensor> hLeftInput;
139    NamedBeanHandle<Sensor> hRightInput;
140
141    // coding used locally to ensure consistency
142    public static final CodeGroupThreeBits CODE_LEFT = CodeGroupThreeBits.Triple100;
143    public static final CodeGroupThreeBits CODE_STOP = CodeGroupThreeBits.Triple010;
144    public static final CodeGroupThreeBits CODE_RIGHT = CodeGroupThreeBits.Triple001;
145    public static final CodeGroupThreeBits CODE_OFF = CodeGroupThreeBits.Triple000;
146
147    // States to track changes at the Code Machine end
148    enum Machine {
149        SET_LEFT,
150        SET_STOP,
151        SET_RIGHT
152    }
153    Machine machine = Machine.SET_STOP;
154
155    CodeGroupThreeBits lastIndication = CODE_STOP;
156    void setLastIndication(CodeGroupThreeBits v) {
157        log.trace("lastIndication goes from {} to {}", lastIndication, v);
158        CodeGroupThreeBits old = lastIndication;
159        lastIndication = v;
160        firePropertyChange("LastIndication", old, lastIndication); // NOI18N
161    }
162    CodeGroupThreeBits getLastIndication() { return lastIndication; }
163
164    boolean timeRunning = false;
165
166    public boolean isRunningTime() { return timeRunning; }
167
168    Station<CodeGroupThreeBits, CodeGroupThreeBits> station;
169    @Override
170    public Station<CodeGroupThreeBits, CodeGroupThreeBits> getStation() { return station;}
171    @Override
172    public String getName() { return "SH for "+hStopIndicator.getBean().getDisplayName(); }
173
174    List<Lock> rightwardLocks;
175    List<Lock> leftwardLocks;
176    public void addRightwardLocks(List<Lock> locks) { this.rightwardLocks = locks; }
177    public void addLeftwardLocks(List<Lock> locks) { this.leftwardLocks = locks; }
178
179    /**
180     * Start of sending code operation:
181     * <ul>
182     * <li>Set indicators off if a change has been requested
183     * <li>Provide values to send over line
184     * </ul>
185     * @return code line value to transmit from machine to field
186     */
187    @Override
188    public CodeGroupThreeBits codeSendStart() {
189        // are we setting to stop, which might start running time?
190        // check for setting to stop while machine has been cleared to left or right
191        if (    (hRightInput.getBean().getKnownState()==Sensor.ACTIVE &&
192                    hLeftIndicator.getBean().getKnownState() == Turnout.THROWN )
193             ||
194                (hLeftInput.getBean().getKnownState()==Sensor.ACTIVE &&
195                    hRightIndicator.getBean().getKnownState() == Turnout.THROWN )
196             ||
197                (hLeftInput.getBean().getKnownState()!=Sensor.ACTIVE && hRightInput.getBean().getKnownState()!=Sensor.ACTIVE &&
198                    ( hRightIndicator.getBean().getKnownState() == Turnout.THROWN || hLeftIndicator.getBean().getKnownState() == Turnout.THROWN) )
199            ) {
200
201            // setting to stop, have to start running time
202            startRunningTime();
203        }
204
205        // Set the indicators based on current and requested state
206        if ( !timeRunning && (
207                  ( machine==Machine.SET_LEFT && hLeftInput.getBean().getKnownState()==Sensor.ACTIVE)
208                || ( machine==Machine.SET_RIGHT && hRightInput.getBean().getKnownState()==Sensor.ACTIVE)
209                || ( machine==Machine.SET_STOP && hRightInput.getBean().getKnownState()!=Sensor.ACTIVE && hLeftInput.getBean().getKnownState()!=Sensor.ACTIVE) )
210                ) {
211            log.debug("No signal change required, states aligned"); // NOI18N
212        } else {
213            log.debug("Signal change requested"); // NOI18N
214            // have to turn off
215            hLeftIndicator.getBean().setCommandedState(Turnout.CLOSED);
216            hStopIndicator.getBean().setCommandedState(Turnout.CLOSED);
217            hRightIndicator.getBean().setCommandedState(Turnout.CLOSED);
218        }
219
220        // return the settings to send
221        CodeGroupThreeBits retval;
222        if (timeRunning) {
223            machine = Machine.SET_STOP;
224            retval = CODE_STOP;
225        } else if (hLeftInput.getBean().getKnownState()==Sensor.ACTIVE) {
226            machine = Machine.SET_LEFT;
227            retval = CODE_LEFT;
228        } else if (hRightInput.getBean().getKnownState()==Sensor.ACTIVE) {
229            machine = Machine.SET_RIGHT;
230            retval = CODE_RIGHT;
231        } else {
232            machine = Machine.SET_STOP;
233            retval = CODE_STOP;
234        }
235        log.debug("codeSendStart returns {}", retval);
236
237        // A model thought -  if setting stop, hold signals immediately
238        // instead of waiting for code cycle.  Model railroads move fast...
239        //if (retval == CODE_STOP) {
240        //    setListHeldState(hRightHeads, true);
241        //    setListHeldState(hLeftHeads, true);
242        //}
243
244        return retval;
245    }
246
247    void startRunningTime() {
248            if (timeRunning) {
249                log.error("Attempt to start running time while it is already running",
250                        LoggingUtil.shortenStacktrace(new Exception("traceback")));
251                return;
252            }
253            timeRunning = true;
254            timeLogSensor.setCommandedState(Sensor.ACTIVE);
255            jmri.util.ThreadingUtil.runOnLayoutDelayed(  ()->{
256                    log.debug("End running time: Station {}", station.getName()); // NOI18N
257                    Lock.signalLockLogger.setStatus(this, "End running time: Station "+station.getName());
258                    timeLogSensor.setCommandedState(Sensor.INACTIVE);
259                    if (!timeRunning) log.warn("Running time timer ended while not marked as running time");
260                    timeRunning = false;
261                    station.requestIndicationStart();
262                } ,
263                (int)timeMemory.getValue());
264
265           Lock.signalLockLogger.setStatus(this, "Running time: Station "+station.getName());
266    }
267
268    public static int MOVEMENT_DELAY = 5000;
269
270    boolean deferIndication = false; // when set, don't indicate on layout change
271                                     // because something else will ensure it later
272
273    /**
274     * Code arrives in field. Sets the signals on the layout.
275     */
276    @Override
277    public void codeValueDelivered(CodeGroupThreeBits value) {
278        // Set signals. While doing that, remember command as indication, so that the
279        // following signal change won't drive an _immediate_ indication cycle.
280        // Also, always go via stop...
281        CodeGroupThreeBits  currentIndication = getCurrentIndication();
282        log.debug("codeValueDelivered sets value {} current: {} last: {}", value, currentIndication, lastIndication);
283
284        if (value == CODE_LEFT && Lock.checkLocksClear(leftwardLocks, Lock.signalLockLogger)) {
285            // setLastIndication(CODE_STOP);
286            // setListHeldState(hRightHeads, true);
287            // setListHeldState(hLeftHeads, true);
288            setLastIndication(CODE_LEFT);
289            log.debug("Layout signals set LEFT"); // NOI18N
290            setListHeldState(hLeftHeads, false);
291            setListHeldState(hRightHeads, true);
292        } else if (value == CODE_RIGHT && Lock.checkLocksClear(rightwardLocks, Lock.signalLockLogger)) {
293            // lastIndication = CODE_STOP;
294            // setListHeldState(hRightHeads, true);
295            // setListHeldState(hLeftHeads, true);
296            setLastIndication(CODE_RIGHT);
297            log.debug("Layout signals set RIGHT"); // NOI18N
298            setListHeldState(hRightHeads, false);
299            setListHeldState(hLeftHeads, true);
300        } else if (value == CODE_STOP) {
301            setLastIndication(CODE_STOP);
302            log.debug("Layout signals set STOP"); // NOI18N
303            setListHeldState(hRightHeads, true);
304            setListHeldState(hLeftHeads, true);
305        } else {
306            // RIGHT or LEFT but locks not clear
307            Lock.signalLockLogger.setStatus(this,
308                "Force stop: left clear "+Lock.checkLocksClear(leftwardLocks, Lock.signalLockLogger)
309                 +", right clear "+Lock.checkLocksClear(rightwardLocks, Lock.signalLockLogger));
310            setLastIndication(CODE_STOP);
311            log.debug("Layout signals set STOP due to locks"); // NOI18N
312            setListHeldState(hRightHeads, true);
313            setListHeldState(hLeftHeads, true);
314        }
315
316        // start the timer for the signals to change
317        if (currentIndication != lastIndication) {
318            log.debug("codeValueDelivered started timer for return indication"); // NOI18N
319            jmri.util.TimerUtil.schedule(new TimerTask() { // NOI18N
320                @Override
321                public void run() {
322                    jmri.util.ThreadingUtil.runOnLayout( ()->{
323                        log.debug("end of movement delay from codeValueDelivered");
324                        station.requestIndicationStart();
325                    } );
326                }
327            }, MOVEMENT_DELAY);
328        }
329        log.debug("End codeValueDelivered");
330    }
331
332    protected void setListHeldState(Iterable<NamedBeanHandle<Signal>> list, boolean state) {
333        for (NamedBeanHandle<Signal> handle : list) {
334            if (handle.getBean().getHeld() != state) handle.getBean().setHeld(state);
335        }
336    }
337
338    @Override
339    public String toString() {
340        StringBuffer retVal = new StringBuffer("SignalHeadSection "); // NOI18N
341
342        retVal.append(" state: "+machine); // NOI18N
343        retVal.append(" time: "+isRunningTime()); // NOI18N
344        retVal.append(" defer: "+deferIndication); // NOI18N
345
346        for (NamedBeanHandle<Signal> handle : hRightHeads) {
347            retVal.append("\n  \"").append(handle.getName()).append("\" "); // NOI18N
348            retVal.append(" held: "+handle.getBean().getHeld()+" "); // NOI18N
349            retVal.append(" clear: "+handle.getBean().isCleared()+" "); // NOI18N
350            retVal.append(" stop: "+handle.getBean().isAtStop()); // NOI18N
351        }
352        for (NamedBeanHandle<Signal> handle : hLeftHeads) {
353            retVal.append("\n  \"").append(handle.getName()).append("\" "); // NOI18N
354            retVal.append(" held: "+handle.getBean().getHeld()+" "); // NOI18N
355            retVal.append(" clear: "+handle.getBean().isCleared()+" "); // NOI18N
356            retVal.append(" stop: "+handle.getBean().isAtStop()); // NOI18N
357        }
358
359        return retVal.toString();
360    }
361
362    /**
363     * Provide state that's returned from field to machine via indication.
364     */
365    @Override
366    public CodeGroupThreeBits indicationStart() {
367        // check for signal drop unexpectedly, other automatic clears
368        // is done in getCurrentIndication()
369        CodeGroupThreeBits retval = getCurrentIndication();
370        log.debug("indicationStart with {}; last indication was {}", retval, lastIndication); // NOI18N
371
372        // TODO: anti-fleeting done always, need call-on logic
373
374
375        setLastIndication(retval);
376        return retval;
377    }
378
379    /**
380     * Clear is defined as showing above Restricting.
381     * We implement that as not Held, not RED, not Restricting.
382     * @param handle signal bean handle.
383     * @return true if clear.
384     */
385    public boolean headShowsClear(NamedBeanHandle<Signal> handle) {
386        return !handle.getBean().getHeld() && handle.getBean().isCleared();
387        }
388
389    /**
390     * "Restricting" means that a signal is showing FLASHRED
391     * @param handle signal bean handle.
392     * @return true if showing restricting.
393     */
394    public boolean headShowsRestricting(NamedBeanHandle<Signal> handle) {
395        return handle.getBean().isShowingRestricting();
396    }
397
398    /**
399     * Work out current indication from layout status.
400     * @return code group.
401     */
402    public CodeGroupThreeBits getCurrentIndication() {
403        log.trace("Start getCurrentIndication with lastIndication {}", lastIndication, LoggingUtil.shortenStacktrace(new Exception("traceback")));
404        boolean leftClear = false;
405        boolean leftHeld = false;
406        boolean leftRestricting = false;
407        for (NamedBeanHandle<Signal> handle : hLeftHeads) {
408            if (headShowsClear(handle)) leftClear = true;
409            if (handle.getBean().getHeld()) leftHeld = true;
410            if (headShowsRestricting(handle)) leftRestricting = true;
411        }
412        boolean rightClear = false;
413        boolean rightHeld = false;
414        boolean rightRestricting = false;
415        for (NamedBeanHandle<Signal> handle : hRightHeads) {
416            if (headShowsClear(handle)) rightClear = true;
417            if (handle.getBean().getHeld()) rightHeld = true;
418            if (headShowsRestricting(handle)) rightRestricting = true;
419        }
420        log.trace(" found  leftClear {},  leftHeld {},  leftRestricting {}, lastIndication {}", leftClear, leftHeld, leftRestricting, lastIndication); // NOI18N
421        log.trace("       rightClear {}, rightHeld {}, rightRestricting {}", rightClear, rightHeld, rightRestricting); // NOI18N
422        if (leftClear && rightClear) log.error("Found both left and right clear: {}", this); // NOI18N
423        if (leftClear && rightRestricting) log.warn("Found left clear and right at restricting: {}", this); // NOI18N
424        if (leftRestricting && rightClear) log.warn("Found left at restricting and right clear: {}", this); // NOI18N
425
426
427        CodeGroupThreeBits retval;
428
429        // Restricting cases show OFF
430        if (leftRestricting || rightRestricting) {
431            Lock.signalLockLogger.setStatus(this, "Force off due to restricting");
432            retval = CODE_OFF;
433        } else if ((!leftClear) && (!rightClear)) {
434            // check for a signal dropping while cleared
435            if (lastIndication != CODE_STOP) {
436                log.debug("CurrentIndication stop due to right and left not clear with {}", lastIndication);
437                Lock.signalLockLogger.setStatus(this, "Show stop due to right and left not clear");
438            } else {
439                Lock.signalLockLogger.clear();
440            }
441            retval = CODE_STOP;
442        } else if ((!leftClear) && rightClear && (lastIndication == CODE_RIGHT  )) {
443            Lock.signalLockLogger.clear();
444            retval = CODE_RIGHT;
445        } else if (leftClear && (!rightClear) && (lastIndication == CODE_LEFT)) {
446            Lock.signalLockLogger.clear();
447            retval = CODE_LEFT;
448        } else {
449            log.debug("Not individually cleared, set OFF");
450            if (!rightClear) Lock.signalLockLogger.setStatus(this, "Force stop due to right not clear");
451            else if (!leftClear) Lock.signalLockLogger.setStatus(this, "Force stop due to left not clear");
452            else Lock.signalLockLogger.setStatus(this, "Force stop due to vital settings");
453            retval = CODE_OFF;
454        }
455        log.trace("End getCurrentIndication returns {}", retval);
456        return retval;
457    }
458
459    /**
460     * Process values received from the field unit.
461     */
462    @Override
463    public void indicationComplete(CodeGroupThreeBits value) {
464        log.debug("indicationComplete sets from {} in state {}", value, machine); // NOI18N
465        if (timeRunning) {
466            hLeftIndicator.getBean().setCommandedState(Turnout.CLOSED);
467            hStopIndicator.getBean().setCommandedState(Turnout.CLOSED);
468            hRightIndicator.getBean().setCommandedState(Turnout.CLOSED);
469        } else switch (value) {
470            case Triple100: // CODE_LEFT
471                hLeftIndicator.getBean().setCommandedState(Turnout.THROWN);
472                hStopIndicator.getBean().setCommandedState(Turnout.CLOSED);
473                hRightIndicator.getBean().setCommandedState(Turnout.CLOSED);
474                break;
475            case Triple010: // CODE_STOP
476                hLeftIndicator.getBean().setCommandedState(Turnout.CLOSED);
477                hStopIndicator.getBean().setCommandedState(Turnout.THROWN);
478                hRightIndicator.getBean().setCommandedState(Turnout.CLOSED);
479                break;
480            case Triple001: // CODE_RIGHT
481                hLeftIndicator.getBean().setCommandedState(Turnout.CLOSED);
482                hStopIndicator.getBean().setCommandedState(Turnout.CLOSED);
483                hRightIndicator.getBean().setCommandedState(Turnout.THROWN);
484                break;
485            case Triple000: // CODE_OFF
486                hLeftIndicator.getBean().setCommandedState(Turnout.CLOSED); // all off
487                hStopIndicator.getBean().setCommandedState(Turnout.CLOSED);
488                hRightIndicator.getBean().setCommandedState(Turnout.CLOSED);
489                break;
490            default:
491                log.error("Got code not recognized: {}", value);
492                hLeftIndicator.getBean().setCommandedState(Turnout.CLOSED);
493                hStopIndicator.getBean().setCommandedState(Turnout.CLOSED);
494                hRightIndicator.getBean().setCommandedState(Turnout.CLOSED);
495                break;
496        }
497    }
498
499    void layoutSignalHeadChanged(java.beans.PropertyChangeEvent e) {
500        CodeGroupThreeBits current = getCurrentIndication();
501        // as a modeling thought, if we're dropping to stop, set held right now
502        log.debug("layoutSignalHeadChanged with last: {} current: {} defer: {}, driving update", lastIndication, current, deferIndication);
503        if (current == CODE_STOP && current != lastIndication && ! deferIndication ) {
504            deferIndication = true;
505            setListHeldState(hRightHeads, true);
506            setListHeldState(hLeftHeads, true);
507            deferIndication = false;
508        }
509
510        // if there was a change, need to send indication back to central
511        if (current != lastIndication && ! deferIndication) {
512            log.debug("  SignalHead change sends changed Indication last: {} current: {} defer: {}, driving update", lastIndication, current, deferIndication);
513            station.requestIndicationStart();
514        } else {
515            log.debug("  SignalHead change without change in Indication");
516        }
517        log.debug("end of layoutSignalHeadChanged");
518    }
519
520    final PropertyChangeSupport pcs = new PropertyChangeSupport(this);
521
522    @OverridingMethodsMustInvokeSuper
523    public synchronized void addPropertyChangeListener(PropertyChangeListener l) {
524        pcs.addPropertyChangeListener(l);
525    }
526
527    @OverridingMethodsMustInvokeSuper
528    public synchronized void removePropertyChangeListener(PropertyChangeListener l) {
529        pcs.removePropertyChangeListener(l);
530    }
531
532    @OverridingMethodsMustInvokeSuper
533    protected void firePropertyChange(String p, Object old, Object n) {
534        pcs.firePropertyChange(p, old, n);
535    }
536
537    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(SignalHeadSection.class);
538}