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++ 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++ 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++ 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++ 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        
091        // Default feedback mode is DIRECT
092        _activeFeedbackType = DIRECT;
093        
094        setModeInformation(_validFeedbackNames, _validFeedbackModes);
095        
096        // set the mode names and values based on the static values.
097        _validFeedbackNames = getModeNames();
098        _validFeedbackModes = getModeValues();
099        
100        // Register to get property change information from the superclass
101        _stateListener = new DCCppTurnoutStateListener(this);
102        this.addPropertyChangeListener(_stateListener);
103        // Finally, request the current state from the layout.
104        tc.getTurnoutReplyCache().requestCachedStateFromLayout(this);
105    }
106
107    //Set the mode information for DCC++ Turnouts.
108    synchronized static private void setModeInformation(String[] feedbackNames, int[] feedbackModes) {
109        // if it hasn't been done already, create static arrays to hold 
110        // the DCC++ specific feedback information.
111        if (modeNames == null) {
112            if (feedbackNames.length != feedbackModes.length) {
113                log.error("int and string feedback arrays different length");
114            }
115            // NOTE: What we are doing here is tacking extra modes to the list
116            // *beyond* the defaults of DIRECT, ONESENSOR and TWOSENSOR
117            modeNames = new String[feedbackNames.length + 2];
118            modeValues = new int[feedbackNames.length + 2];
119            for (int i = 0; i < feedbackNames.length; i++) {
120                modeNames[i] = feedbackNames[i];
121                modeValues[i] = feedbackModes[i];
122            }
123            modeNames[feedbackNames.length] = "BSTURNOUT";
124            modeValues[feedbackNames.length] = MONITORING;
125            modeNames[feedbackNames.length+1] = "BSOUTPUT";
126            modeValues[feedbackNames.length+1] = EXACT;
127        }
128    }
129
130    static int[] getModeValues() {
131        return modeValues;
132    }
133
134    static String[] getModeNames() {
135        return modeNames;
136    }
137
138    public int getNumber() {
139        return mNumber;
140    }
141
142    /**
143     * Set the Commanded State.
144     * This method overides {@link jmri.implementation.AbstractTurnout#setCommandedState(int)}.
145     */
146    @Override
147    public void setCommandedState(int s) {
148        log.debug("set commanded state for turnout {} to {}", getSystemName(), s);
149
150        synchronized (this) {
151            newCommandedState(s);
152        }
153        forwardCommandChangeToLayout(s);
154        // Only set the known state to inconsistent if we actually expect a response
155        // from the Base Station
156        if (_activeFeedbackType == EXACT || _activeFeedbackType == MONITORING) {
157            synchronized (this) {
158                newKnownState(INCONSISTENT);
159            }
160        } else if( _activeFeedbackType == DIRECT ){
161            synchronized (this) {
162                newKnownState(s);
163            }
164        }
165    }
166
167    /**
168     * {@inheritDoc}
169     * Sends a DCC++ command.
170     */
171    @Override
172    synchronized protected void forwardCommandChangeToLayout(int s) {
173        DCCppMessage msg;
174        if (s != CLOSED && s != THROWN) {
175            log.warn("Turnout {}: state {} not forwarded to layout.", mNumber, s);
176            return;
177        }
178        // newState = TRUE if s == THROWN ...
179        // ... unless we are inverted, then newState = TRUE if s == CLOSED
180        boolean newState = (s == THROWN);
181        if (getInverted()) {
182            newState = !newState;
183        }
184        switch (_activeFeedbackType) {
185            case EXACT: // Use <z ... > command
186                // mNumber is the index ID into the Base Station's internal table of outputs.
187                // Convert the integer Turnout value to boolean for DCC++ internal code.
188                // Assume if it's not THROWN (true), it must be CLOSED (false).
189                // Note for Outputs (EXACT mode), LOW is THROWN, HIGH is CLOSED
190                // As defined in DCC++ Base Station SerialCommand.cpp, so newstate
191                // is inverted when making the message
192                msg = DCCppMessage.makeOutputCmdMsg(mNumber, !newState);
193                internalState = COMMANDSENT;
194                break;
195            case MONITORING: // Use <T ... > command
196                // mNumber is the index ID into the Base Station's internal table of Turnouts.
197                // Convert the integer Turnout value to boolean for DCC++ internal code.
198                // Assume if it's not THROWN (true), it must be CLOSED (false).
199                msg = DCCppMessage.makeTurnoutCommandMsg(mNumber, newState);
200                internalState = COMMANDSENT;
201                break;
202            default: // DIRECT -- use <a ... > command
203                // mNumber is the DCC address of the device.
204                // Convert the integer Turnout value to boolean for DCC++ internal code.
205                // Assume if it's not THROWN (true), it must be CLOSED (false).
206                msg = DCCppMessage.makeAccessoryDecoderMsg(mNumber, newState);
207            internalState = IDLE;
208                break;
209        }
210        log.debug("Sending Message: '{}'", msg);
211        tc.sendDCCppMessage(msg, null);  // status returned via manager
212    }
213    
214    @Override
215    protected void turnoutPushbuttonLockout(boolean _pushButtonLockout) {
216        log.debug("Send command to {} Pushbutton {}T{}", (_pushButtonLockout ? "Lock" : "Unlock"), _prefix, mNumber);
217    }
218    
219    /**
220     * request an update on status by sending a DCC++ message
221     */
222    @Override
223    public void requestUpdateFromLayout() {
224        // This will handle query for ONESENSOR and TWOSENSOR feedback modes.
225        super.requestUpdateFromLayout();
226        // (02/2017) Yes it does... using the <s> command or possibly
227        // some others.  TODO: Plumb this in... IFF it is needed.
228        /*
229        // DCCppMessage msg = DCCppMessage.getFeedbackRequestMsg(mNumber,
230        //         ((mNumber - 1) % 4) < 2);
231        // synchronized (this) {
232        //     internalState = STATUSREQUESTSENT;
233        // }
234        // tc.sendDCCppMessage(msg, null); //status is returned via the manager.
235        */
236
237    }
238
239    @Override
240    public boolean canInvert() {
241        return true;
242    }
243
244    /**
245     * initmessage is a package proteceted class which allows the Manger to send
246     * a feedback message at initialization without changing the state of the
247     * turnout with respect to whether or not a feedback request was sent. This
248     * is used only when the turnout is created by on layout feedback.
249     *
250     * @param l Init message
251     */
252    synchronized void initmessage(DCCppReply l) {
253        int oldState = internalState;
254        message(l);
255        internalState = oldState;
256    }
257
258    /*
259     *  Handle an incoming message from the DCC++
260     */
261    @Override
262    synchronized public void message(DCCppReply l) {
263        //if this is a turnout definition message, copy the defining properties from message to turnout
264        if (l.isTurnoutDefDCCReply() || l.isTurnoutDefServoReply() || l.isTurnoutDefVpinReply()  || l.isTurnoutDefLCNReply() ) {
265            l.getProperties().forEach((key, value) -> {
266                this.setProperty(key, value); //copy the properties
267            });
268        }
269        
270        switch (getFeedbackMode()) {
271        case EXACT:
272            handleExactModeFeedback(l);
273            break;
274        case MONITORING:
275            handleMonitoringModeFeedback(l);
276            break;
277        case DIRECT:
278        default:
279            // Default is direct mode - we should never get here, actually.
280        }
281    }
282
283    // Listen for the outgoing messages (to the command station)
284    @Override
285    public void message(DCCppMessage l) {
286    }
287
288    // Handle a timeout notification
289    @Override
290    public void notifyTimeout(DCCppMessage msg) {
291        log.debug("Notified of timeout on message '{}'", msg);
292    }
293
294    /*
295     *  With Monitoring Mode feedback, if we see a feedback message, we 
296     *  interpret that message and use it to display our feedback. 
297     *  <p>
298     *  After we send a request to operate a turnout, We ask the command 
299     *  station to stop sending information to the stationary decoder
300     *  when the either a feedback message or an "OK" message is received.
301     *
302     *  @param l a {@link DCCppReply} message
303     */
304    synchronized private void handleMonitoringModeFeedback(DCCppReply l) {
305        log.debug("Handle Message for turnout {} in MONITORING feedback mode", mNumber);
306        if (l.isTurnoutReply() && (l.getTOIDInt() == mNumber)) {
307           if (l.getTOIsThrown()) {
308               log.debug("Turnout is Thrown. Inverted = {}", (getInverted() ? "True" : "False"));
309               synchronized (this) {
310                   newCommandedState(getInverted() ? CLOSED : THROWN);
311                   newKnownState(getCommandedState());
312               }
313           } else if (l.getTOIsClosed()) {
314               log.debug("Turnout is Closed. Inverted = {}", (getInverted() ? "True" : "False"));
315               synchronized (this) {
316                   newCommandedState(getInverted() ? THROWN : CLOSED);
317                   newKnownState(getCommandedState());
318               }
319           }
320           internalState = IDLE;
321        }
322        return;
323    }
324    
325    synchronized private void handleExactModeFeedback(DCCppReply l) {
326        /* 
327           Note for Outputs (EXACT mode), LOW is THROWN, HIGH is CLOSED
328           As defined in DCC++ Base Station SerialCommand.cpp
329        */
330        log.debug("Handle Message for turnout {} in EXACT feedback mode", mNumber);
331        if (l.isOutputCmdReply() && (l.getOutputNumInt() == mNumber)) {
332           if (l.getOutputIsLow()) {
333               log.debug("Turnout is Thrown. Inverted = {}", (getInverted() ? "True" : "False"));
334               synchronized (this) {
335                   newCommandedState(getInverted() ? CLOSED : THROWN);
336                   newKnownState(getCommandedState());
337               }
338           } else if (l.getOutputIsHigh()) {
339               log.debug("Turnout is Closed. Inverted = {}", (getInverted() ? "True" : "False"));
340               synchronized (this) {
341                   newCommandedState(getInverted() ? THROWN : CLOSED);
342                   newKnownState(getCommandedState());
343               }
344           }
345           internalState = IDLE;
346        }
347        return;
348    }
349 
350    @Override
351    public void dispose() {
352        this.removePropertyChangeListener(_stateListener);
353        super.dispose();
354    }
355    
356    // Internal class to use for listening to state changes
357    private static class DCCppTurnoutStateListener implements java.beans.PropertyChangeListener {
358        
359        DCCppTurnout _turnout = null;
360        
361        DCCppTurnoutStateListener(DCCppTurnout turnout) {
362            _turnout = turnout;
363        }
364        
365        /*
366         * If we're  not using DIRECT feedback mode, we need to listen for 
367         * state changes to know when to send an OFF message after we set the 
368         * known state
369         * If we're using DIRECT mode, all of this is handled from the 
370         * outgoing Messages
371         */
372        @Override
373        public void propertyChange(java.beans.PropertyChangeEvent event) {
374            log.debug("propertyChange called");
375            // If we're using DIRECT feedback mode, we don't care what we see here
376            if (_turnout.getFeedbackMode() != DIRECT) {
377                if (log.isDebugEnabled()) {
378                    log.debug("propertyChange Not Direct Mode property: {} old value {} new value {}", event.getPropertyName(), event.getOldValue(), event.getNewValue());
379                }
380                if (event.getPropertyName().equals("KnownState")) {
381                    // Check to see if this is a change in the status 
382                    // triggered by a device on the layout, or a change in 
383                    // status we triggered.
384                    int oldKnownState = ((Integer) event.getOldValue()).intValue();
385                    int curKnownState = ((Integer) event.getNewValue()).intValue();
386                    log.debug("propertyChange KnownState - old value {} new value {}", oldKnownState, curKnownState);
387                    if (curKnownState != INCONSISTENT
388                        && _turnout.getCommandedState() == oldKnownState) {
389                        // This was triggered by feedback on the layout, change 
390                        // the commanded state to reflect the new Known State
391                        if (log.isDebugEnabled()) {
392                            log.debug("propertyChange CommandedState: {}", _turnout.getCommandedState());
393                        }
394                        _turnout.newCommandedState(curKnownState);
395                    } else {
396                        // Since we always set the KnownState to 
397                        // INCONSISTENT when we send a command, If the old 
398                        // known state is INCONSISTENT, we just want to send 
399                        // an off message
400                        if (oldKnownState == INCONSISTENT) {
401                            if (log.isDebugEnabled()) {
402                                log.debug("propertyChange CommandedState: {}", _turnout.getCommandedState());
403                            }
404                        }
405                    }
406                }
407            }
408        }
409        
410    }
411    
412    // data members
413    protected int mNumber;   // turnout number
414    DCCppTurnoutStateListener _stateListener;  // Internal class object
415    
416    private final static Logger log = LoggerFactory.getLogger(DCCppTurnout.class);
417    
418}