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    /**
236     * Set the turnout DCC C bit to OFF. This is typically used to set a C bit
237     * that was set ON to OFF after a timeout.
238     *
239     * @param state the turnout state
240     */
241    void sendSetOffMessage(int state) {
242        sendOpcSwReqMessage(adjustStateForInversion(state), false);
243    }
244
245    private void handleReceivedOpSwAckReq(LocoNetMessage l) {
246        int sw2 = l.getElement(2);
247        if (myAddress(l.getElement(1), sw2)) {
248
249            log.debug("SW_REQ received with valid address");
250            //sort out states
251            int state;
252            state = ((sw2 & LnConstants.OPC_SW_REQ_DIR) != 0) ? CLOSED : THROWN;
253            state = adjustStateForInversion(state);
254
255            newCommandedState(state);
256            computeKnownStateOpSwAckReq(sw2, state);
257        }
258    }
259
260    private void computeKnownStateOpSwAckReq(int sw2, int state) {
261        boolean on = ((sw2 & LnConstants.OPC_SW_REQ_OUT) != 0);
262        switch (getFeedbackMode()) {
263            case MONITORING:
264                if ((!on) || (!_useOffSwReqAsConfirmation)) {
265                    newKnownState(state);
266                }
267                break;
268            case DIRECT:
269                newKnownState(state);
270                break;
271            default:
272                break;
273        }
274
275    }
276    private void setKnownStateFromOutputStateClosedReport() {
277        newCommandedState(CLOSED);
278        if (getFeedbackMode() == MONITORING || getFeedbackMode() == DIRECT) {
279            newKnownState(CLOSED);
280        } else if (getFeedbackMode() == LNALTERNATE) {
281            newKnownState(adjustStateForInversion(CLOSED));
282        }
283    }
284
285    private void setKnownStateFromOutputStateThrownReport() {
286        newCommandedState(THROWN);
287        if (getFeedbackMode() == MONITORING || getFeedbackMode() == DIRECT) {
288            newKnownState(THROWN);
289        } else if (getFeedbackMode() == LNALTERNATE) {
290            newKnownState(adjustStateForInversion(THROWN));
291        }
292    }
293
294    private void setKnownStateFromOutputStateOddReport() {
295        newCommandedState(CLOSED + THROWN);
296        if (getFeedbackMode() == MONITORING || getFeedbackMode() == DIRECT) {
297            newKnownState(CLOSED + THROWN);
298        }
299    }
300
301    private void setKnownStateFromOutputStateReallyOddReport() {
302        newCommandedState(0);
303        if (getFeedbackMode() == MONITORING || getFeedbackMode() == DIRECT) {
304            newKnownState(0);
305        } else if (getFeedbackMode() == LNALTERNATE) {
306            newKnownState(INCONSISTENT);
307        }
308    }
309
310    private void computeFromOutputStateReport(int sw2) {
311        // LnConstants.OPC_SW_REP_INPUTS not set, these report outputs
312        // sort out states
313        int state;
314        state = sw2
315                & (LnConstants.OPC_SW_REP_CLOSED | LnConstants.OPC_SW_REP_THROWN);
316        state = adjustStateForInversion(state);
317
318        switch (state) {
319            case LnConstants.OPC_SW_REP_CLOSED:
320                setKnownStateFromOutputStateClosedReport();
321                break;
322            case LnConstants.OPC_SW_REP_THROWN:
323                setKnownStateFromOutputStateThrownReport();
324                break;
325            case LnConstants.OPC_SW_REP_CLOSED | LnConstants.OPC_SW_REP_THROWN:
326                setKnownStateFromOutputStateOddReport();
327                break;
328            default:
329                setKnownStateFromOutputStateReallyOddReport();
330                break;
331        }
332    }
333
334    private void computeFeedbackFromSwitchReport(int sw2) {
335        // Switch input report
336        if ((sw2 & LnConstants.OPC_SW_REP_HI) != 0) {
337            computeFeedbackFromSwitchOffReport();
338        } else {
339            computeFeedbackFromSwitchOnReport();
340        }
341    }
342
343    private void computeFeedbackFromSwitchOffReport() {
344        // switch input closed (off)
345        if (getFeedbackMode() == EXACT) {
346            // reached closed state
347            newKnownState(adjustStateForInversion(CLOSED));
348        } else if (getFeedbackMode() == INDIRECT) {
349            // reached closed state
350            newKnownState(adjustStateForInversion(CLOSED));
351        } else if (!feedbackDeliberatelySet) {
352            // don't have a defined feedback mode, but know we've reached closed state
353            log.debug("setting CLOSED with !feedbackDeliberatelySet");
354            newKnownState(adjustStateForInversion(CLOSED));
355        }
356    }
357
358    private void computeFeedbackFromSwitchOnReport() {
359        // switch input thrown (input on)
360        if (getFeedbackMode() == EXACT) {
361            // leaving CLOSED on way to THROWN, go INCONSISTENT if not already THROWN
362            if (getKnownState() != THROWN) {
363                newKnownState(INCONSISTENT);
364            }
365        } else if (getFeedbackMode() == INDIRECT) {
366            // reached thrown state
367            newKnownState(adjustStateForInversion(THROWN));
368        } else if (!feedbackDeliberatelySet) {
369            // don't have a defined feedback mode, but know we're not in closed state, most likely is actually thrown
370            log.debug("setting THROWN with !feedbackDeliberatelySet");
371            newKnownState(adjustStateForInversion(THROWN));
372        }
373    }
374
375    private void computeFromSwFeedbackState(int sw2) {
376        // LnConstants.OPC_SW_REP_INPUTS set, these are feedback messages from inputs
377        // sort out states
378        if ((sw2 & LnConstants.OPC_SW_REP_SW) != 0) {
379            computeFeedbackFromSwitchReport(sw2);
380
381        } else {
382            computeFeedbackFromAuxInputReport(sw2);
383        }
384    }
385
386    private void computeFeedbackFromAuxInputReport(int sw2) {
387        // This is only valid in EXACT mode, so if we encounter it
388        //  without a feedback mode set, we switch to EXACT
389        if (!feedbackDeliberatelySet) {
390            setFeedbackMode(EXACT);
391            feedbackDeliberatelySet = false; // was set when setting feedback
392        }
393
394        if ((sw2 & LnConstants.OPC_SW_REP_HI) != 0) {
395            // aux input closed (off)
396            if (getFeedbackMode() == EXACT) {
397                // reached thrown state
398                newKnownState(adjustStateForInversion(THROWN));
399            }
400        } else {
401            // aux input thrown (input on)
402            if (getFeedbackMode() == EXACT) {
403                // leaving THROWN on the way to CLOSED, go INCONSISTENT if not already CLOSED
404                if (getKnownState() != CLOSED) {
405                    newKnownState(INCONSISTENT);
406                }
407            }
408        }
409    }
410
411    private void handleReceivedOpSwRep(LocoNetMessage l) {
412        int sw1 = l.getElement(1);
413        int sw2 = l.getElement(2);
414        if (myAddress(sw1, sw2)) {
415
416            log.debug("SW_REP received with valid address");
417            // see if its a turnout state report
418            if ((sw2 & LnConstants.OPC_SW_REP_INPUTS) == 0) {
419                computeFromOutputStateReport(sw2);
420            } else {
421                computeFromSwFeedbackState(sw2);
422            }
423        }
424    }
425
426    // implementing classes will typically have a function/listener to get
427    // updates from the layout, which will then call
428    //        public void firePropertyChange(String propertyName,
429    //                              Object oldValue,
430    //                        Object newValue)
431    // _once_ if anything has changed state (or set the commanded state directly)
432    public void messageFromManager(LocoNetMessage l) {
433        // parse message type
434        switch (l.getOpCode()) {
435            case LnConstants.OPC_SW_ACK:
436            case LnConstants.OPC_SW_REQ: {
437                handleReceivedOpSwAckReq(l);
438                return;
439                }
440            case LnConstants.OPC_SW_REP: {
441                handleReceivedOpSwRep(l);
442                return;
443            }
444            default:
445                return;
446        }
447    }
448
449    /** {@inheritDoc} */
450    @Override
451    public void requestUpdateFromLayout() {
452        if (_activeFeedbackType == MONITORING || _activeFeedbackType == INDIRECT) {
453            LocoNetMessage l = new LocoNetMessage(4);
454            l.setOpCode(LnConstants.OPC_SW_STATE);
455            l.setElement(1, (_number-1) & 0x7f);
456            l.setElement(2, (_number-1) >> 7);
457            this.controller.sendLocoNetMessage(l);  // send message
458        } else {
459            super.requestUpdateFromLayout();
460        }
461    }
462
463    @Override
464    protected void turnoutPushbuttonLockout(boolean _pushButtonLockout) {
465        if (log.isDebugEnabled()) {
466            log.debug("Send command to {} Pushbutton {}T{}", (_pushButtonLockout ? "Lock" : "Unlock"), _prefix, _number);
467        }
468    }
469
470    @Override
471    public void dispose() {
472        if(meterTask!=null) {
473           meterTask.cancel();
474        }
475        if(consistencyTask != null ) {
476           consistencyTask.cancel();
477        }
478        super.dispose();
479    }
480
481    // data members
482    int _number;   // LocoNet Turnout number
483
484    private boolean myAddress(int a1, int a2) {
485        // the "+ 1" in the following converts to throttle-visible numbering
486        return (((a2 & 0x0f) * 128) + (a1 & 0x7f) + 1) == _number;
487    }
488
489    //ln turnouts do support inversion
490    @Override
491    public boolean canInvert() {
492        return true;
493    }
494
495    /**
496     * Take a turnout state as a parameter and adjusts it as necessary
497     * to reflect the turnout "Invert" property.
498     *
499     * @param rawState "original" turnout state before optional inverting
500     */
501    private int adjustStateForInversion(int rawState) {
502
503        if (getInverted() && (rawState == CLOSED || rawState == THROWN)) {
504            if (rawState == CLOSED) {
505                return THROWN;
506            } else {
507                return CLOSED;
508            }
509        } else {
510            return rawState;
511        }
512    }
513
514    static final int METERINTERVAL = 100;  // msec wait before closed
515    private java.util.TimerTask meterTask = null;
516
517    static final int CONSISTENCYTIMER = 3000; // msec wait for command to take effect
518    int noConsistencyTimersRunning = 0;
519    private java.util.TimerTask consistencyTask = null;
520
521    private final static Logger log = LoggerFactory.getLogger(LnTurnout.class);
522
523}