001package jmri.jmrix.loconet;
002
003import javax.annotation.*;
004import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
005import jmri.NmraPacket;
006import jmri.implementation.AbstractTurnout;
007import org.slf4j.Logger;
008import org.slf4j.LoggerFactory;
009
010/**
011 * Extend jmri.AbstractTurnout for LocoNet layouts
012 * <p>
013 * This implementation implements the "SENT" feedback, where LocoNet messages
014 * originating on the layout can change both KnownState and CommandedState. We
015 * change both because we consider a LocoNet message to reflect how the turnout
016 * should be, even if it's a readback status message. E.g. if you use a DS54
017 * local input to change the state, resulting in a status message, we still
018 * consider that to be a commanded state change.
019 * <p>
020 * Adds several additional feedback modes:
021 * <ul>
022 *   <li>MONITORING - listen to the LocoNet, so that commands from other LocoNet
023 *   sources (e.g. throttles) are properly reflected in the turnout state. This is
024 *   the default for LnTurnout objects as created.
025 *   <li>INDIRECT - listen to the LocoNet for messages back from a DS54 that has a
026 *   microswitch attached to its Switch input.
027 *   <li>EXACT - listen to the LocoNet for messages back from a DS54 that has two
028 *   microswitches, one connected to the Switch input and one to the Aux input.
029 *   <li>ALTERNATE - listen to the LocoNet for messages back from a MGP decoders
030 *   that has reports servo moving.
031 * </ul>
032 * Some of the message formats used in this class are Copyright Digitrax, Inc.
033 * and used with permission as part of the JMRI project. That permission does
034 * not extend to uses in other software products. If you wish to use this code,
035 * algorithm or these message formats outside of JMRI, please contact Digitrax
036 * Inc for separate permission.
037 *
038 * @author Bob Jacobsen Copyright (C) 2001
039 */
040public class LnTurnout extends AbstractTurnout {
041
042    public LnTurnout(String prefix, int number, LocoNetInterface controller) throws IllegalArgumentException {
043        // a human-readable turnout number must be specified!
044        super(prefix + "T" + number);  // can't use prefix here, as still in construction
045        _prefix = prefix;
046        log.debug("new turnout {}", number);
047        if (number < NmraPacket.accIdLowLimit || number > NmraPacket.accIdAltHighLimit) {
048            throw new IllegalArgumentException("Turnout value: " + number // NOI18N
049                    + " not in the range " + NmraPacket.accIdLowLimit + " to " // NOI18N
050                    + NmraPacket.accIdAltHighLimit);
051        }
052
053        this.controller = controller;
054
055        _number = number;
056        // update feedback modes
057        _validFeedbackTypes |= MONITORING | EXACT | INDIRECT | LNALTERNATE ;
058        _activeFeedbackType = MONITORING;
059
060        // if needed, create the list of feedback mode
061        // names with additional LocoNet-specific modes
062        if (modeNames == null) {
063            initFeedbackModes();
064        }
065        _validFeedbackNames = modeNames;
066        _validFeedbackModes = modeValues;
067    }
068
069    LocoNetInterface controller;
070    protected String _prefix = "L"; // default to "L"
071
072    /**
073     * True when setFeedbackMode has specified the mode;
074     * false when the mode is just left over from initialization.
075     * This is intended to indicate (when true) that a configuration
076     * file has set the value; message-created turnouts have it false.
077     */
078    boolean feedbackDeliberatelySet = false; // package to allow access from LnTurnoutManager
079
080    @Override
081    public void setBinaryOutput(boolean state) {
082        // TODO Auto-generated method stub
083        setProperty(LnTurnoutManager.SENDONANDOFFKEY, !state);
084        binaryOutput = state;
085    }
086    @Override
087    public void setFeedbackMode(@Nonnull String mode) throws IllegalArgumentException {
088        feedbackDeliberatelySet = true;
089        super.setFeedbackMode(mode);
090    }
091
092    @Override
093    public void setFeedbackMode(int mode) throws IllegalArgumentException {
094        feedbackDeliberatelySet = true;
095        super.setFeedbackMode(mode);
096    }
097
098    @SuppressFBWarnings(value = "ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD",
099            justification = "Only used during creation of 1st turnout") // NOI18N
100    private void initFeedbackModes() {
101        if (_validFeedbackNames.length != _validFeedbackModes.length) {
102            log.error("int and string feedback arrays different length");
103        }
104        String[] tempModeNames = new String[_validFeedbackNames.length + 4];
105        int[] tempModeValues = new int[_validFeedbackNames.length + 4];
106        for (int i = 0; i < _validFeedbackNames.length; i++) {
107            tempModeNames[i] = _validFeedbackNames[i];
108            tempModeValues[i] = _validFeedbackModes[i];
109        }
110        tempModeNames[_validFeedbackNames.length] = "MONITORING"; // NOI18N
111        tempModeValues[_validFeedbackNames.length] = MONITORING;
112        tempModeNames[_validFeedbackNames.length + 1] = "INDIRECT"; // NOI18N
113        tempModeValues[_validFeedbackNames.length + 1] = INDIRECT;
114        tempModeNames[_validFeedbackNames.length + 2] = "EXACT"; // NOI18N
115        tempModeValues[_validFeedbackNames.length + 2] = EXACT;
116        tempModeNames[_validFeedbackNames.length + 3] = "LNALTERNATE"; // NOI18N
117        tempModeValues[_validFeedbackNames.length + 3] = LNALTERNATE;
118
119        modeNames = tempModeNames;
120        modeValues = tempModeValues;
121    }
122
123    static String[] modeNames = null;
124    static int[] modeValues = null;
125
126    public int getNumber() {
127        return _number;
128    }
129
130    boolean _useOffSwReqAsConfirmation = false;
131
132    public void setUseOffSwReqAsConfirmation(boolean state) {
133        _useOffSwReqAsConfirmation = state;
134    }
135
136    public boolean isByPassBushbyBit() {
137        Object returnVal = getProperty(LnTurnoutManager.BYPASSBUSHBYBITKEY);
138        if (returnVal == null) {
139            return  false;
140        }
141        return (boolean) returnVal;
142    }
143
144    public boolean isSendOnAndOff() {
145        Object returnVal = getProperty(LnTurnoutManager.SENDONANDOFFKEY);
146        if (returnVal == null) {
147            return  true;
148        }
149        return (boolean) returnVal;
150    }
151
152    /**
153     * {@inheritDoc}
154     */
155    @Override
156    protected void forwardCommandChangeToLayout(final int newstate) {
157
158        // send SWREQ for close/thrown ON
159        sendOpcSwReqMessage(adjustStateForInversion(newstate), true);
160        // schedule SWREQ for closed/thrown off, unless in basic mode
161        if (isSendOnAndOff()) {
162            meterTask = new java.util.TimerTask() {
163                int state = newstate;
164
165                @Override
166                public void run() {
167                    try {
168                        sendSetOffMessage(state);
169                    } catch (Exception e) {
170                        log.error("Exception occurred while sending delayed off to turnout", e);
171                    }
172                }
173            };
174            jmri.util.TimerUtil.schedule(meterTask, METERINTERVAL);
175        }
176    }
177
178    /**
179     * Send a single OPC_SW_REQ message for this turnout, with the CLOSED/THROWN
180     * ON/OFF state.
181     * <p>
182     * Inversion is to already have been handled.
183     *
184     * @param state the state to set
185     * @param on    if true the C bit of the NMRA DCC packet is 1; if false the
186     *              C bit is 0
187     */
188    void sendOpcSwReqMessage(int state, boolean on) {
189        LocoNetMessage l = new LocoNetMessage(4);
190        l.setOpCode(isByPassBushbyBit() ? LnConstants.OPC_SW_ACK : LnConstants.OPC_SW_REQ);
191        int hiadr = ((_number - 1) / 128) & 0x7F;   // compute address fields
192        l.setElement(1, ((_number - 1) - hiadr * 128) & 0x7F);
193
194        // set closed bit (Note that LocoNet cannot handle both Thrown and Closed)
195        if ((state & CLOSED) != 0) {
196            hiadr |= 0x20;
197            // thrown exception if also THROWN
198            if ((state & THROWN) != 0) {
199                log.error("LocoNet turnout logic can't handle both THROWN and CLOSED yet");
200            }
201        }
202
203        // load On/Off
204        if (on) {
205            hiadr |= 0x10;
206        } else if (_useOffSwReqAsConfirmation) {
207            log.warn("Turnout {} is using OPC_SWREQ off as confirmation, but is sending OFF commands itself anyway", _number);
208        }
209
210        l.setElement(2, hiadr);
211
212        this.controller.sendLocoNetMessage(l);  // send message
213
214        if (_useOffSwReqAsConfirmation) {
215            noConsistencyTimersRunning++;
216            startConsistencyTimerTask();
217        }
218    }
219
220    private void startConsistencyTimerTask() {
221        // Start a timer to resend the command in a couple of seconds in case consistency is not obtained before then
222        consistencyTask = new java.util.TimerTask() {
223            @Override
224            public void run() {
225                noConsistencyTimersRunning--;
226                if (!isConsistentState() && noConsistencyTimersRunning == 0) {
227                    log.debug("LnTurnout resending command for turnout {}", _number);
228                    forwardCommandChangeToLayout(getCommandedState());
229                }
230            }
231        };
232        jmri.util.TimerUtil.schedule(consistencyTask, CONSISTENCYTIMER);
233    }
234
235    boolean pending = false;
236
237    /**
238     * Set the turnout DCC C bit to OFF. This is typically used to set a C bit
239     * that was set ON to OFF after a timeout.
240     *
241     * @param state the turnout state
242     */
243    void sendSetOffMessage(int state) {
244        sendOpcSwReqMessage(adjustStateForInversion(state), false);
245    }
246
247    private void handleReceivedOpSwAckReq(LocoNetMessage l) {
248        int sw2 = l.getElement(2);
249        if (myAddress(l.getElement(1), sw2)) {
250
251            log.debug("SW_REQ received with valid address");
252            //sort out states
253            int state;
254            state = ((sw2 & LnConstants.OPC_SW_REQ_DIR) != 0) ? CLOSED : THROWN;
255            state = adjustStateForInversion(state);
256
257            newCommandedState(state);
258            computeKnownStateOpSwAckReq(sw2, state);
259        }
260    }
261
262    private void computeKnownStateOpSwAckReq(int sw2, int state) {
263        boolean on = ((sw2 & LnConstants.OPC_SW_REQ_OUT) != 0);
264        switch (getFeedbackMode()) {
265            case MONITORING:
266                if ((!on) || (!_useOffSwReqAsConfirmation)) {
267                    newKnownState(state);
268                }
269                break;
270            case DIRECT:
271                newKnownState(state);
272                break;
273            default:
274                break;
275        }
276
277    }
278    private void setKnownStateFromOutputStateClosedReport() {
279        newCommandedState(CLOSED);
280        if (getFeedbackMode() == MONITORING || getFeedbackMode() == DIRECT) {
281            newKnownState(CLOSED);
282        } else if (getFeedbackMode() == LNALTERNATE) {
283            newKnownState(adjustStateForInversion(CLOSED));
284        }
285    }
286
287    private void setKnownStateFromOutputStateThrownReport() {
288        newCommandedState(THROWN);
289        if (getFeedbackMode() == MONITORING || getFeedbackMode() == DIRECT) {
290            newKnownState(THROWN);
291        } else if (getFeedbackMode() == LNALTERNATE) {
292            newKnownState(adjustStateForInversion(THROWN));
293        }
294    }
295
296    private void setKnownStateFromOutputStateOddReport() {
297        newCommandedState(CLOSED + THROWN);
298        if (getFeedbackMode() == MONITORING || getFeedbackMode() == DIRECT) {
299            newKnownState(CLOSED + THROWN);
300        }
301    }
302
303    private void setKnownStateFromOutputStateReallyOddReport() {
304        newCommandedState(0);
305        if (getFeedbackMode() == MONITORING || getFeedbackMode() == DIRECT) {
306            newKnownState(0);
307        } else if (getFeedbackMode() == LNALTERNATE) {
308            newKnownState(INCONSISTENT);
309        }
310    }
311
312    private void computeFromOutputStateReport(int sw2) {
313        // LnConstants.OPC_SW_REP_INPUTS not set, these report outputs
314        // sort out states
315        int state;
316        state = sw2
317                & (LnConstants.OPC_SW_REP_CLOSED | LnConstants.OPC_SW_REP_THROWN);
318        state = adjustStateForInversion(state);
319
320        switch (state) {
321            case LnConstants.OPC_SW_REP_CLOSED:
322                setKnownStateFromOutputStateClosedReport();
323                break;
324            case LnConstants.OPC_SW_REP_THROWN:
325                setKnownStateFromOutputStateThrownReport();
326                break;
327            case LnConstants.OPC_SW_REP_CLOSED | LnConstants.OPC_SW_REP_THROWN:
328                setKnownStateFromOutputStateOddReport();
329                break;
330            default:
331                setKnownStateFromOutputStateReallyOddReport();
332                break;
333        }
334    }
335
336    private void computeFeedbackFromSwitchReport(int sw2) {
337        // Switch input report
338        if ((sw2 & LnConstants.OPC_SW_REP_HI) != 0) {
339            computeFeedbackFromSwitchOffReport();
340        } else {
341            computeFeedbackFromSwitchOnReport();
342        }
343    }
344
345    private void computeFeedbackFromSwitchOffReport() {
346        // switch input closed (off)
347        if (getFeedbackMode() == EXACT) {
348            // reached closed state
349            newKnownState(adjustStateForInversion(CLOSED));
350        } else if (getFeedbackMode() == INDIRECT) {
351            // reached closed state
352            newKnownState(adjustStateForInversion(CLOSED));
353        } else if (!feedbackDeliberatelySet) {
354            // don't have a defined feedback mode, but know we've reached closed state
355            log.debug("setting CLOSED with !feedbackDeliberatelySet");
356            newKnownState(adjustStateForInversion(CLOSED));
357        }
358    }
359
360    private void computeFeedbackFromSwitchOnReport() {
361        // switch input thrown (input on)
362        if (getFeedbackMode() == EXACT) {
363            // leaving CLOSED on way to THROWN, go INCONSISTENT if not already THROWN
364            if (getKnownState() != THROWN) {
365                newKnownState(INCONSISTENT);
366            }
367        } else if (getFeedbackMode() == INDIRECT) {
368            // reached thrown state
369            newKnownState(adjustStateForInversion(THROWN));
370        } else if (!feedbackDeliberatelySet) {
371            // don't have a defined feedback mode, but know we're not in closed state, most likely is actually thrown
372            log.debug("setting THROWN with !feedbackDeliberatelySet");
373            newKnownState(adjustStateForInversion(THROWN));
374        }
375    }
376
377    private void computeFromSwFeedbackState(int sw2) {
378        // LnConstants.OPC_SW_REP_INPUTS set, these are feedback messages from inputs
379        // sort out states
380        if ((sw2 & LnConstants.OPC_SW_REP_SW) != 0) {
381            computeFeedbackFromSwitchReport(sw2);
382
383        } else {
384            computeFeedbackFromAuxInputReport(sw2);
385        }
386    }
387
388    private void computeFeedbackFromAuxInputReport(int sw2) {
389        // This is only valid in EXACT mode, so if we encounter it
390        //  without a feedback mode set, we switch to EXACT
391        if (!feedbackDeliberatelySet) {
392            setFeedbackMode(EXACT);
393            feedbackDeliberatelySet = false; // was set when setting feedback
394        }
395
396        if ((sw2 & LnConstants.OPC_SW_REP_HI) != 0) {
397            // aux input closed (off)
398            if (getFeedbackMode() == EXACT) {
399                // reached thrown state
400                newKnownState(adjustStateForInversion(THROWN));
401            }
402        } else {
403            // aux input thrown (input on)
404            if (getFeedbackMode() == EXACT) {
405                // leaving THROWN on the way to CLOSED, go INCONSISTENT if not already CLOSED
406                if (getKnownState() != CLOSED) {
407                    newKnownState(INCONSISTENT);
408                }
409            }
410        }
411    }
412
413    private void handleReceivedOpSwRep(LocoNetMessage l) {
414        int sw1 = l.getElement(1);
415        int sw2 = l.getElement(2);
416        if (myAddress(sw1, sw2)) {
417
418            log.debug("SW_REP received with valid address");
419            // see if its a turnout state report
420            if ((sw2 & LnConstants.OPC_SW_REP_INPUTS) == 0) {
421                computeFromOutputStateReport(sw2);
422            } else {
423                computeFromSwFeedbackState(sw2);
424            }
425        }
426    }
427
428    // implementing classes will typically have a function/listener to get
429    // updates from the layout, which will then call
430    //        public void firePropertyChange(String propertyName,
431    //                              Object oldValue,
432    //                        Object newValue)
433    // _once_ if anything has changed state (or set the commanded state directly)
434    public void messageFromManager(LocoNetMessage l) {
435        // parse message type
436        switch (l.getOpCode()) {
437            case LnConstants.OPC_SW_ACK:
438            case LnConstants.OPC_SW_REQ: {
439                handleReceivedOpSwAckReq(l);
440                return;
441                }
442            case LnConstants.OPC_SW_REP: {
443                handleReceivedOpSwRep(l);
444                return;
445            }
446            default:
447                return;
448        }
449    }
450
451    @Override
452    protected void turnoutPushbuttonLockout(boolean _pushButtonLockout) {
453        if (log.isDebugEnabled()) {
454            log.debug("Send command to {} Pushbutton {}T{}", (_pushButtonLockout ? "Lock" : "Unlock"), _prefix, _number);
455        }
456    }
457
458    @Override
459    public void dispose() {
460        if(meterTask!=null) {
461           meterTask.cancel();
462        }
463        if(consistencyTask != null ) {
464           consistencyTask.cancel();
465        }
466        super.dispose();
467    }
468
469    // data members
470    int _number;   // LocoNet Turnout number
471
472    private boolean myAddress(int a1, int a2) {
473        // the "+ 1" in the following converts to throttle-visible numbering
474        return (((a2 & 0x0f) * 128) + (a1 & 0x7f) + 1) == _number;
475    }
476
477    //ln turnouts do support inversion
478    @Override
479    public boolean canInvert() {
480        return true;
481    }
482
483    /**
484     * Take a turnout state as a parameter and adjusts it as necessary
485     * to reflect the turnout "Invert" property.
486     *
487     * @param rawState "original" turnout state before optional inverting
488     */
489    private int adjustStateForInversion(int rawState) {
490
491        if (getInverted() && (rawState == CLOSED || rawState == THROWN)) {
492            if (rawState == CLOSED) {
493                return THROWN;
494            } else {
495                return CLOSED;
496            }
497        } else {
498            return rawState;
499        }
500    }
501
502    static final int METERINTERVAL = 100;  // msec wait before closed
503    private java.util.TimerTask meterTask = null;
504
505    static final int CONSISTENCYTIMER = 3000; // msec wait for command to take effect
506    int noConsistencyTimersRunning = 0;
507    private java.util.TimerTask consistencyTask = null;
508
509    private final static Logger log = LoggerFactory.getLogger(LnTurnout.class);
510
511}