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        myOperator = getTurnoutOperator(); // MUST set myOperator before starting the thread
157        if (myOperator == null) {
158            forwardCommandChangeToLayout(s);
159            // Only set the known state to inconsistent if we actually expect a response
160            // from the Base Station
161            if (_activeFeedbackType == EXACT || _activeFeedbackType == MONITORING) {
162                synchronized (this) {
163                    newKnownState(INCONSISTENT);
164                }
165            } else if (_activeFeedbackType == DIRECT || _activeFeedbackType == CS_VPIN) {
166                // CS_VPIN: no guaranteed reply from <z>; update optimistically.
167                // An EXRAIL-broadcast <Y> reply may also update KnownState later.
168                synchronized (this) {
169                    newKnownState(s);
170                }
171            }
172        } else {
173            myOperator.start();
174        }
175    }
176
177    /**
178     * {@inheritDoc}
179     * Sends a DCC-EX command.
180     */
181    @Override
182    synchronized protected void forwardCommandChangeToLayout(int s) {
183        DCCppMessage msg;
184        if (s != CLOSED && s != THROWN) {
185            log.warn("Turnout {}: state {} not forwarded to layout.", mNumber, s);
186            return;
187        }
188        // newState = TRUE if s == THROWN ...
189        // ... unless we are inverted, then newState = TRUE if s == CLOSED
190        boolean newState = (s == THROWN);
191        if (getInverted()) {
192            newState = !newState;
193        }
194        switch (_activeFeedbackType) {
195            case EXACT: // Use <Z ... > command
196                // mNumber is the index ID into the Base Station's internal table of outputs.
197                // Convert the integer Turnout value to boolean for DCC-EX internal code.
198                // Assume if it's not THROWN (true), it must be CLOSED (false).
199                // Note for Outputs (EXACT mode), LOW is THROWN, HIGH is CLOSED
200                // As defined in DCC-EX Base Station SerialCommand.cpp, so newstate
201                // is inverted when making the message
202                msg = DCCppMessage.makeOutputCmdMsg(mNumber, !newState);
203                internalState = COMMANDSENT;
204                break;
205            case CS_VPIN: // Use <z vpin> / <z -vpin> pin control command
206                // mNumber is the DCC-EX vpin number; no pre-definition required.
207                // Polarity matches EXACT: LOW = THROWN, HIGH = CLOSED — invert newState.
208                msg = DCCppMessage.makeOutputCmdMsgLC(mNumber, !newState);
209                internalState = IDLE; // no guaranteed reply
210                break;
211            case MONITORING: // Use <T ... > command
212                // mNumber is the index ID into the Base Station's internal table of Turnouts.
213                // Convert the integer Turnout value to boolean for DCC-EX internal code.
214                // Assume if it's not THROWN (true), it must be CLOSED (false).
215                msg = DCCppMessage.makeTurnoutCommandMsg(mNumber, newState);
216                internalState = COMMANDSENT;
217                break;
218            default: // DIRECT -- use <a ... > command
219                // mNumber is the DCC address of the device.
220                // Convert the integer Turnout value to boolean for DCC-EX internal code.
221                // Assume if it's not THROWN (true), it must be CLOSED (false).
222                msg = DCCppMessage.makeAccessoryDecoderMsg(mNumber, newState);
223            internalState = IDLE;
224                break;
225        }
226        log.debug("Sending Message: '{}'", msg);
227        tc.sendDCCppMessage(msg, null);  // status returned via manager
228    }
229    
230    @Override
231    protected void turnoutPushbuttonLockout(boolean _pushButtonLockout) {
232        log.debug("Send command to {} Pushbutton {}T{}", (_pushButtonLockout ? "Lock" : "Unlock"), _prefix, mNumber);
233    }
234    
235    /**
236     * request an update on status by sending a DCC-EX message
237     */
238    @Override
239    public void requestUpdateFromLayout() {
240        // This will handle query for ONESENSOR and TWOSENSOR feedback modes.
241        super.requestUpdateFromLayout();
242        // (02/2017) Yes it does... using the <s> command or possibly
243        // some others.  TODO: Plumb this in... IFF it is needed.
244        /*
245        // DCCppMessage msg = DCCppMessage.getFeedbackRequestMsg(mNumber,
246        //         ((mNumber - 1) % 4) < 2);
247        // synchronized (this) {
248        //     internalState = STATUSREQUESTSENT;
249        // }
250        // tc.sendDCCppMessage(msg, null); //status is returned via the manager.
251        */
252
253    }
254
255    @Override
256    public boolean canInvert() {
257        return true;
258    }
259
260    /**
261     * initmessage is a package proteceted class which allows the Manger to send
262     * a feedback message at initialization without changing the state of the
263     * turnout with respect to whether or not a feedback request was sent. This
264     * is used only when the turnout is created by on layout feedback.
265     *
266     * @param l Init message
267     */
268    synchronized void initmessage(DCCppReply l) {
269        int oldState = internalState;
270        message(l);
271        internalState = oldState;
272    }
273
274    /*
275     *  Handle an incoming message from the DCC-EX
276     */
277    @Override
278    synchronized public void message(DCCppReply l) {
279        //if this is a turnout definition message, copy the defining properties from message to turnout
280        if (l.isTurnoutDefDCCReply() || l.isTurnoutDefServoReply() || l.isTurnoutDefVpinReply()  || l.isTurnoutDefLCNReply() ) {
281            l.getProperties().forEach((key, value) -> {
282                this.setProperty(key, value); //copy the properties
283            });
284        }
285        
286        switch (getFeedbackMode()) {
287        case EXACT:
288            handleExactModeFeedback(l);
289            break;
290        case MONITORING:
291            handleMonitoringModeFeedback(l);
292            break;
293        case DIRECT:
294        default:
295            // Default is direct mode - we should never get here, actually.
296        }
297    }
298
299    // Listen for the outgoing messages (to the command station)
300    @Override
301    public void message(DCCppMessage l) {
302    }
303
304    // Handle a timeout notification
305    @Override
306    public void notifyTimeout(DCCppMessage msg) {
307        log.debug("Notified of timeout on message '{}'", msg);
308    }
309
310    /*
311     *  With Monitoring Mode feedback, if we see a feedback message, we 
312     *  interpret that message and use it to display our feedback. 
313     *  <p>
314     *  After we send a request to operate a turnout, We ask the command 
315     *  station to stop sending information to the stationary decoder
316     *  when the either a feedback message or an "OK" message is received.
317     *
318     *  @param l a {@link DCCppReply} message
319     */
320    synchronized private void handleMonitoringModeFeedback(DCCppReply l) {
321        log.debug("Handle Message for turnout {} in MONITORING feedback mode", mNumber);
322        if (l.isTurnoutReply() && (l.getTOIDInt() == mNumber)) {
323           if (l.getTOIsThrown()) {
324               log.debug("Turnout is Thrown. Inverted = {}", (getInverted() ? "True" : "False"));
325               synchronized (this) {
326                   newCommandedState(getInverted() ? CLOSED : THROWN);
327                   newKnownState(getCommandedState());
328               }
329           } else if (l.getTOIsClosed()) {
330               log.debug("Turnout is Closed. Inverted = {}", (getInverted() ? "True" : "False"));
331               synchronized (this) {
332                   newCommandedState(getInverted() ? THROWN : CLOSED);
333                   newKnownState(getCommandedState());
334               }
335           }
336           internalState = IDLE;
337        }
338        return;
339    }
340    
341    synchronized private void handleExactModeFeedback(DCCppReply l) {
342        /* 
343           Note for Outputs (EXACT mode), LOW is THROWN, HIGH is CLOSED
344           As defined in DCC-EX Base Station SerialCommand.cpp
345        */
346        log.debug("Handle Message for turnout {} in EXACT feedback mode", mNumber);
347        if (l.isOutputCmdReply() && (l.getOutputNumInt() == mNumber)) {
348           if (l.getOutputIsLow()) {
349               log.debug("Turnout is Thrown. Inverted = {}", (getInverted() ? "True" : "False"));
350               synchronized (this) {
351                   newCommandedState(getInverted() ? CLOSED : THROWN);
352                   newKnownState(getCommandedState());
353               }
354           } else if (l.getOutputIsHigh()) {
355               log.debug("Turnout is Closed. Inverted = {}", (getInverted() ? "True" : "False"));
356               synchronized (this) {
357                   newCommandedState(getInverted() ? THROWN : CLOSED);
358                   newKnownState(getCommandedState());
359               }
360           }
361           internalState = IDLE;
362        }
363        return;
364    }
365 
366    @Override
367    public void dispose() {
368        this.removePropertyChangeListener(_stateListener);
369        super.dispose();
370    }
371    
372    // Internal class to use for listening to state changes
373    private static class DCCppTurnoutStateListener implements java.beans.PropertyChangeListener {
374        
375        DCCppTurnout _turnout = null;
376        
377        DCCppTurnoutStateListener(DCCppTurnout turnout) {
378            _turnout = turnout;
379        }
380        
381        /*
382         * If we're  not using DIRECT feedback mode, we need to listen for 
383         * state changes to know when to send an OFF message after we set the 
384         * known state
385         * If we're using DIRECT mode, all of this is handled from the 
386         * outgoing Messages
387         */
388        @Override
389        public void propertyChange(java.beans.PropertyChangeEvent event) {
390            log.debug("propertyChange called");
391            // If we're using DIRECT feedback mode, we don't care what we see here
392            if (_turnout.getFeedbackMode() != DIRECT) {
393                if (log.isDebugEnabled()) {
394                    log.debug("propertyChange Not Direct Mode property: {} old value {} new value {}", event.getPropertyName(), event.getOldValue(), event.getNewValue());
395                }
396                if (event.getPropertyName().equals("KnownState")) {
397                    // Check to see if this is a change in the status 
398                    // triggered by a device on the layout, or a change in 
399                    // status we triggered.
400                    int oldKnownState = ((Integer) event.getOldValue()).intValue();
401                    int curKnownState = ((Integer) event.getNewValue()).intValue();
402                    log.debug("propertyChange KnownState - old value {} new value {}", oldKnownState, curKnownState);
403                    if (curKnownState != INCONSISTENT
404                        && _turnout.getCommandedState() == oldKnownState) {
405                        // This was triggered by feedback on the layout, change 
406                        // the commanded state to reflect the new Known State
407                        if (log.isDebugEnabled()) {
408                            log.debug("propertyChange CommandedState: {}", _turnout.getCommandedState());
409                        }
410                        _turnout.newCommandedState(curKnownState);
411                    } else {
412                        // Since we always set the KnownState to 
413                        // INCONSISTENT when we send a command, If the old 
414                        // known state is INCONSISTENT, we just want to send 
415                        // an off message
416                        if (oldKnownState == INCONSISTENT) {
417                            if (log.isDebugEnabled()) {
418                                log.debug("propertyChange CommandedState: {}", _turnout.getCommandedState());
419                            }
420                        }
421                    }
422                }
423            }
424        }
425        
426    }
427    
428    // data members
429    protected int mNumber;   // turnout number
430    DCCppTurnoutStateListener _stateListener;  // Internal class object
431    
432    private static final Logger log = LoggerFactory.getLogger(DCCppTurnout.class);
433    
434}