001package jmri.jmrix.dccpp;
002
003import jmri.implementation.AbstractTurnout;
004import org.slf4j.Logger;
005import org.slf4j.LoggerFactory;
006
007/**
008 * Extends jmri.AbstractTurnout for DCCpp layouts
009 * <p>
010 * Turnouts on DCC-EX are controlled (as of V1.5 Firmware)
011 * with unidirectional Stationary Decoder commands, or with bidirectional
012 * (predefined) Turnout commands, or with bidirectional (predefined) Output
013 * commands.
014 * 
015 * DCC-EX Has three ways to activate a turnout (output)
016 * <ul>
017 * <li> Accessory Command "a" : sends a DCC packet to a stationary decoder
018 *      out there on the bus somewhere. NO RETURN VALUE to JMRI.
019 * </li>
020 * <li> Turnout Command "T" : Looks up a DCC address from an internal table
021 *      in the Base Station and sends that Stationary Decoder a packet.  Returns
022 *      a (basically faked) "H" response to JMRI indicating the (supposed)
023 *      current state of the turnout.  Or "X" if the indexed turnout is not in
024 *      the list.
025 * </li>
026 * <li> Output Command "z" : Looks up a Base Station Arduino Pin number from
027 *      an internal lookup table, and sets/toggles the state of that pin.  
028 *      Returns a "Y" response indicating the actual state of the pin.  Or "X"
029 *      if the indexed pin is not in the list.
030 * </li>
031 * </ul>
032 * 
033 * The DCCppTurnout supports three types of feedback:
034 * <ul>
035 * <li> DIRECT:  No actual feedback, uses Stationary Decoder command and
036 *      fakes the response.
037 * </li>
038 * <li> MONITORING: Uses the Turnout command, lets the Base Station
039 *      fake the response :) 
040 * </li>
041 * <li> EXACT: Uses the Output command to directly address an Arduino pin.
042 * </li>
043 * </ul>
044 *
045 * It also supports "NO FEEDBACK" by treating it like "DIRECT".
046 * 
047 * Turnout operation on DCC-EX based systems goes through the following
048 * sequence:
049 * <ul>
050 * <li> set the commanded state, and, Send request to command station to start
051 * sending DCC operations packet to track</li>
052 * </ul>
053 *
054 * @author Bob Jacobsen Copyright (C) 2001
055 * @author Paul Bender Copyright (C) 2003-2010
056 * @author Mark Underwood Copyright (C) 2015
057 *
058 * Based on lenz.XNetTurnout by Bob Jacobsen and Paul Bender
059 */
060public class DCCppTurnout extends AbstractTurnout implements DCCppListener {
061
062    /* State information */
063    protected static final int COMMANDSENT = 2;
064    protected static final int STATUSREQUESTSENT = 4;
065    protected static final int IDLE = 0;
066    protected int internalState = IDLE;
067
068    /* Static arrays to hold DCC-EX specific feedback mode information */
069    static String[] modeNames = null;
070    static int[] modeValues = null;
071
072    //@SuppressFBWarnings(value = "IS2_INCONSISTENT_SYNC")
073    //protected int _mThrown = jmri.Turnout.THROWN;
074    //@SuppressFBWarnings(value = "IS2_INCONSISTENT_SYNC")
075    //protected int _mClosed = jmri.Turnout.CLOSED;
076
077    protected String _prefix = "D"; // default
078    protected DCCppTrafficController tc = null;
079
080    public DCCppTurnout(String prefix, int pNumber, DCCppTrafficController controller) {  // a human-readable turnout number must be specified!
081        super(prefix + "T" + pNumber);
082        tc = controller;
083        _prefix = prefix;
084        mNumber = pNumber; // this is the address.
085
086        /* Add additional feedback types information */
087        // Note DIRECT, ONESENSOR and TWOSENSOR are already OR'ed in.
088        _validFeedbackTypes |= MONITORING;   // uses the Turnout command <T...>
089        _validFeedbackTypes |= EXACT;        // uses the Output command <Z...>
090        _validFeedbackTypes |= CS_VPIN;    // uses the pin control command <z...>
091        
092        // Default feedback mode is DIRECT
093        _activeFeedbackType = DIRECT;
094        
095        setModeInformation(_validFeedbackNames, _validFeedbackModes);
096        
097        // set the mode names and values based on the static values.
098        _validFeedbackNames = getModeNames();
099        _validFeedbackModes = getModeValues();
100        
101        // Register to get property change information from the superclass
102        _stateListener = new DCCppTurnoutStateListener(this);
103        this.addPropertyChangeListener(_stateListener);
104        // Finally, request the current state from the layout.
105        tc.getTurnoutReplyCache().requestCachedStateFromLayout(this);
106    }
107
108    //Set the mode information for DCC-EX Turnouts.
109    synchronized private static void setModeInformation(String[] feedbackNames, int[] feedbackModes) {
110        // if it hasn't been done already, create static arrays to hold
111        // the DCC-EX specific feedback information.
112        if (modeNames == null) {
113            if (feedbackNames.length != feedbackModes.length) {
114                log.error("int and string feedback arrays different length");
115            }
116            // NOTE: What we are doing here is tacking extra modes to the list
117            // *beyond* the defaults of DIRECT, ONESENSOR and TWOSENSOR
118            modeNames = new String[feedbackNames.length + 3];
119            modeValues = new int[feedbackNames.length + 3];
120            for (int i = 0; i < feedbackNames.length; i++) {
121                modeNames[i] = feedbackNames[i];
122                modeValues[i] = feedbackModes[i];
123            }
124            modeNames[feedbackNames.length] = "BSTURNOUT";
125            modeValues[feedbackNames.length] = MONITORING;
126            modeNames[feedbackNames.length+1] = "BSOUTPUT";
127            modeValues[feedbackNames.length+1] = EXACT;
128            modeNames[feedbackNames.length+2] = "CS VPIN";
129            modeValues[feedbackNames.length+2] = CS_VPIN;
130        }
131    }
132
133    static int[] getModeValues() {
134        return modeValues;
135    }
136
137    static String[] getModeNames() {
138        return modeNames;
139    }
140
141    public int getNumber() {
142        return mNumber;
143    }
144
145    /**
146     * Set the Commanded State.
147     * This method overides {@link jmri.implementation.AbstractTurnout#setCommandedState(int)}.
148     */
149    @Override
150    public void setCommandedState(int s) {
151        log.debug("set commanded state for turnout {} to {}", getSystemName(), s);
152
153        synchronized (this) {
154            newCommandedState(s);
155        }
156        forwardCommandChangeToLayout(s);
157        // Only set the known state to inconsistent if we actually expect a response
158        // from the Base Station
159        if (_activeFeedbackType == EXACT || _activeFeedbackType == MONITORING) {
160            synchronized (this) {
161                newKnownState(INCONSISTENT);
162            }
163        } else if (_activeFeedbackType == DIRECT || _activeFeedbackType == CS_VPIN) {
164            // CS_VPIN: no guaranteed reply from <z>; update optimistically.
165            // An EXRAIL-broadcast <Y> reply may also update KnownState later.
166            synchronized (this) {
167                newKnownState(s);
168            }
169        }
170    }
171
172    /**
173     * {@inheritDoc}
174     * Sends a DCC-EX command.
175     */
176    @Override
177    synchronized protected void forwardCommandChangeToLayout(int s) {
178        DCCppMessage msg;
179        if (s != CLOSED && s != THROWN) {
180            log.warn("Turnout {}: state {} not forwarded to layout.", mNumber, s);
181            return;
182        }
183        // newState = TRUE if s == THROWN ...
184        // ... unless we are inverted, then newState = TRUE if s == CLOSED
185        boolean newState = (s == THROWN);
186        if (getInverted()) {
187            newState = !newState;
188        }
189        switch (_activeFeedbackType) {
190            case EXACT: // Use <Z ... > command
191                // mNumber is the index ID into the Base Station's internal table of outputs.
192                // Convert the integer Turnout value to boolean for DCC-EX internal code.
193                // Assume if it's not THROWN (true), it must be CLOSED (false).
194                // Note for Outputs (EXACT mode), LOW is THROWN, HIGH is CLOSED
195                // As defined in DCC-EX Base Station SerialCommand.cpp, so newstate
196                // is inverted when making the message
197                msg = DCCppMessage.makeOutputCmdMsg(mNumber, !newState);
198                internalState = COMMANDSENT;
199                break;
200            case CS_VPIN: // Use <z vpin> / <z -vpin> pin control command
201                // mNumber is the DCC-EX vpin number; no pre-definition required.
202                // Polarity matches EXACT: LOW = THROWN, HIGH = CLOSED — invert newState.
203                msg = DCCppMessage.makeOutputCmdMsgLC(mNumber, !newState);
204                internalState = IDLE; // no guaranteed reply
205                break;
206            case MONITORING: // Use <T ... > command
207                // mNumber is the index ID into the Base Station's internal table of Turnouts.
208                // Convert the integer Turnout value to boolean for DCC-EX internal code.
209                // Assume if it's not THROWN (true), it must be CLOSED (false).
210                msg = DCCppMessage.makeTurnoutCommandMsg(mNumber, newState);
211                internalState = COMMANDSENT;
212                break;
213            default: // DIRECT -- use <a ... > command
214                // mNumber is the DCC address of the device.
215                // Convert the integer Turnout value to boolean for DCC-EX internal code.
216                // Assume if it's not THROWN (true), it must be CLOSED (false).
217                msg = DCCppMessage.makeAccessoryDecoderMsg(mNumber, newState);
218            internalState = IDLE;
219                break;
220        }
221        log.debug("Sending Message: '{}'", msg);
222        tc.sendDCCppMessage(msg, null);  // status returned via manager
223    }
224    
225    @Override
226    protected void turnoutPushbuttonLockout(boolean _pushButtonLockout) {
227        log.debug("Send command to {} Pushbutton {}T{}", (_pushButtonLockout ? "Lock" : "Unlock"), _prefix, mNumber);
228    }
229    
230    /**
231     * request an update on status by sending a DCC-EX message
232     */
233    @Override
234    public void requestUpdateFromLayout() {
235        // This will handle query for ONESENSOR and TWOSENSOR feedback modes.
236        super.requestUpdateFromLayout();
237        // (02/2017) Yes it does... using the <s> command or possibly
238        // some others.  TODO: Plumb this in... IFF it is needed.
239        /*
240        // DCCppMessage msg = DCCppMessage.getFeedbackRequestMsg(mNumber,
241        //         ((mNumber - 1) % 4) < 2);
242        // synchronized (this) {
243        //     internalState = STATUSREQUESTSENT;
244        // }
245        // tc.sendDCCppMessage(msg, null); //status is returned via the manager.
246        */
247
248    }
249
250    @Override
251    public boolean canInvert() {
252        return true;
253    }
254
255    /**
256     * initmessage is a package proteceted class which allows the Manger to send
257     * a feedback message at initialization without changing the state of the
258     * turnout with respect to whether or not a feedback request was sent. This
259     * is used only when the turnout is created by on layout feedback.
260     *
261     * @param l Init message
262     */
263    synchronized void initmessage(DCCppReply l) {
264        int oldState = internalState;
265        message(l);
266        internalState = oldState;
267    }
268
269    /*
270     *  Handle an incoming message from the DCC-EX
271     */
272    @Override
273    synchronized public void message(DCCppReply l) {
274        //if this is a turnout definition message, copy the defining properties from message to turnout
275        if (l.isTurnoutDefDCCReply() || l.isTurnoutDefServoReply() || l.isTurnoutDefVpinReply()  || l.isTurnoutDefLCNReply() ) {
276            l.getProperties().forEach((key, value) -> {
277                this.setProperty(key, value); //copy the properties
278            });
279        }
280        
281        switch (getFeedbackMode()) {
282        case EXACT:
283            handleExactModeFeedback(l);
284            break;
285        case MONITORING:
286            handleMonitoringModeFeedback(l);
287            break;
288        case DIRECT:
289        default:
290            // Default is direct mode - we should never get here, actually.
291        }
292    }
293
294    // Listen for the outgoing messages (to the command station)
295    @Override
296    public void message(DCCppMessage l) {
297    }
298
299    // Handle a timeout notification
300    @Override
301    public void notifyTimeout(DCCppMessage msg) {
302        log.debug("Notified of timeout on message '{}'", msg);
303    }
304
305    /*
306     *  With Monitoring Mode feedback, if we see a feedback message, we 
307     *  interpret that message and use it to display our feedback. 
308     *  <p>
309     *  After we send a request to operate a turnout, We ask the command 
310     *  station to stop sending information to the stationary decoder
311     *  when the either a feedback message or an "OK" message is received.
312     *
313     *  @param l a {@link DCCppReply} message
314     */
315    synchronized private void handleMonitoringModeFeedback(DCCppReply l) {
316        log.debug("Handle Message for turnout {} in MONITORING feedback mode", mNumber);
317        if (l.isTurnoutReply() && (l.getTOIDInt() == mNumber)) {
318           if (l.getTOIsThrown()) {
319               log.debug("Turnout is Thrown. Inverted = {}", (getInverted() ? "True" : "False"));
320               synchronized (this) {
321                   newCommandedState(getInverted() ? CLOSED : THROWN);
322                   newKnownState(getCommandedState());
323               }
324           } else if (l.getTOIsClosed()) {
325               log.debug("Turnout is Closed. Inverted = {}", (getInverted() ? "True" : "False"));
326               synchronized (this) {
327                   newCommandedState(getInverted() ? THROWN : CLOSED);
328                   newKnownState(getCommandedState());
329               }
330           }
331           internalState = IDLE;
332        }
333        return;
334    }
335    
336    synchronized private void handleExactModeFeedback(DCCppReply l) {
337        /* 
338           Note for Outputs (EXACT mode), LOW is THROWN, HIGH is CLOSED
339           As defined in DCC-EX Base Station SerialCommand.cpp
340        */
341        log.debug("Handle Message for turnout {} in EXACT feedback mode", mNumber);
342        if (l.isOutputCmdReply() && (l.getOutputNumInt() == mNumber)) {
343           if (l.getOutputIsLow()) {
344               log.debug("Turnout is Thrown. Inverted = {}", (getInverted() ? "True" : "False"));
345               synchronized (this) {
346                   newCommandedState(getInverted() ? CLOSED : THROWN);
347                   newKnownState(getCommandedState());
348               }
349           } else if (l.getOutputIsHigh()) {
350               log.debug("Turnout is Closed. Inverted = {}", (getInverted() ? "True" : "False"));
351               synchronized (this) {
352                   newCommandedState(getInverted() ? THROWN : CLOSED);
353                   newKnownState(getCommandedState());
354               }
355           }
356           internalState = IDLE;
357        }
358        return;
359    }
360 
361    @Override
362    public void dispose() {
363        this.removePropertyChangeListener(_stateListener);
364        super.dispose();
365    }
366    
367    // Internal class to use for listening to state changes
368    private static class DCCppTurnoutStateListener implements java.beans.PropertyChangeListener {
369        
370        DCCppTurnout _turnout = null;
371        
372        DCCppTurnoutStateListener(DCCppTurnout turnout) {
373            _turnout = turnout;
374        }
375        
376        /*
377         * If we're  not using DIRECT feedback mode, we need to listen for 
378         * state changes to know when to send an OFF message after we set the 
379         * known state
380         * If we're using DIRECT mode, all of this is handled from the 
381         * outgoing Messages
382         */
383        @Override
384        public void propertyChange(java.beans.PropertyChangeEvent event) {
385            log.debug("propertyChange called");
386            // If we're using DIRECT feedback mode, we don't care what we see here
387            if (_turnout.getFeedbackMode() != DIRECT) {
388                if (log.isDebugEnabled()) {
389                    log.debug("propertyChange Not Direct Mode property: {} old value {} new value {}", event.getPropertyName(), event.getOldValue(), event.getNewValue());
390                }
391                if (event.getPropertyName().equals("KnownState")) {
392                    // Check to see if this is a change in the status 
393                    // triggered by a device on the layout, or a change in 
394                    // status we triggered.
395                    int oldKnownState = ((Integer) event.getOldValue()).intValue();
396                    int curKnownState = ((Integer) event.getNewValue()).intValue();
397                    log.debug("propertyChange KnownState - old value {} new value {}", oldKnownState, curKnownState);
398                    if (curKnownState != INCONSISTENT
399                        && _turnout.getCommandedState() == oldKnownState) {
400                        // This was triggered by feedback on the layout, change 
401                        // the commanded state to reflect the new Known State
402                        if (log.isDebugEnabled()) {
403                            log.debug("propertyChange CommandedState: {}", _turnout.getCommandedState());
404                        }
405                        _turnout.newCommandedState(curKnownState);
406                    } else {
407                        // Since we always set the KnownState to 
408                        // INCONSISTENT when we send a command, If the old 
409                        // known state is INCONSISTENT, we just want to send 
410                        // an off message
411                        if (oldKnownState == INCONSISTENT) {
412                            if (log.isDebugEnabled()) {
413                                log.debug("propertyChange CommandedState: {}", _turnout.getCommandedState());
414                            }
415                        }
416                    }
417                }
418            }
419        }
420        
421    }
422    
423    // data members
424    protected int mNumber;   // turnout number
425    DCCppTurnoutStateListener _stateListener;  // Internal class object
426    
427    private static final Logger log = LoggerFactory.getLogger(DCCppTurnout.class);
428    
429}