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