001package jmri.jmrix.marklin;
002
003import jmri.Turnout;
004import jmri.implementation.AbstractTurnout;
005
006/**
007 * Implement a Turnout via Marklin communications.
008 * <p>
009 * This object doesn't listen to the Marklin communications. This is because it
010 * should be the only object that is sending messages for this turnout; more
011 * than one Turnout object pointing to a single device is not allowed.
012 * <p>
013 * Based on work by Bob Jacobsen
014 *
015 * @author Kevin Dickerson Copyright (C) 2012
016 *
017 */
018public class MarklinTurnout extends AbstractTurnout implements MarklinListener {
019
020    /**
021     * Marklin turnouts use the NMRA number (0-2040) as their numerical
022     * identification in the system name.
023     *
024     * @param number address of the turnout
025     * @param prefix system prefix
026     * @param etc connection traffic controller
027     */
028    public MarklinTurnout(int number, String prefix, MarklinTrafficController etc) {
029        super(prefix + "T" + number);
030        _number = number;
031        tc = etc;
032        tc.addMarklinListener(MarklinTurnout.this);
033    }
034
035    private final MarklinTrafficController tc;
036
037    /**
038     * {@inheritDoc}
039     */
040    @Override
041    protected void forwardCommandChangeToLayout(int newState) {
042        // implementing classes will typically have a function/listener to get
043        // updates from the layout, which will then call
044        //  public void firePropertyChange(String propertyName,
045        //          Object oldValue,
046        //          Object newValue)
047        // _once_ if anything has changed state (or set the commanded state directly)
048
049        // sort out states
050        if ((newState & Turnout.CLOSED) != 0) {
051            // first look for the double case, which we can't handle
052            if ((newState & Turnout.THROWN) != 0) {
053                // this is the disaster case!
054                log.error("Cannot command both CLOSED and THROWN {}", newState);
055            } else {
056                // send a CLOSED command
057                sendMessage(!getInverted());
058            }
059        } else {
060            // send a THROWN command
061            sendMessage(getInverted());
062        }
063    }
064
065    // data members
066    private final int _number;   // turnout number
067
068    /**
069     * Set the turnout known state to reflect what's been observed from the
070     * command station messages. A change there means that somebody commanded a
071     * state change (by using a throttle), and that command has
072     * already taken effect. Hence we use "newCommandedState" to indicate it's
073     * taken place. Must be followed by "newKnownState" to complete the turnout
074     * action.
075     *
076     * @param state Observed state, updated state from command station
077     */
078    synchronized void setCommandedStateFromCS(int state) {
079        if ((getFeedbackMode() != DIRECT)) {
080            return;
081        }
082
083        newCommandedState(state);
084    }
085
086    /**
087     * Set the turnout known state to reflect what's been observed from the
088     * command station messages. A change there means that somebody commanded a
089     * state change (by using a throttle), and that command has
090     * already taken effect. Hence we use "newKnownState" to indicate it's taken
091     * place.
092     *
093     * @param state Observed state, updated state from command station
094     */
095    synchronized void setKnownStateFromCS(int state) {
096        newCommandedState(state);
097        if (getFeedbackMode() == DIRECT) {
098            newKnownState(state);
099        }
100    }
101
102    @Override
103    public void turnoutPushbuttonLockout(boolean b) {
104    }
105
106    /**
107     * Marklin turnouts can be inverted
108     */
109    @Override
110    public boolean canInvert() {
111        return true;
112    }
113
114    static final int PROTOCOL_UNKNOWN = MarklinConstants.PROTOCOL_UNKNOWN;
115    static final int DCC = MarklinConstants.PROTOCOL_DCC;
116    static final int MM2 = MarklinConstants.PROTOCOL_MM2;
117    static final int SFX = MarklinConstants.PROTOCOL_SX;
118
119    private int protocol = PROTOCOL_UNKNOWN;
120
121    /**
122     * Tell the layout to go to new state.
123     *
124     * @param newstate State of the turnout to be sent to the command station
125     */
126    protected void sendMessage(final boolean newstate) {
127        MarklinMessage m = MarklinMessage.getSetTurnout(getCANAddress(), (newstate ? 1 : 0), 0x01);
128        tc.sendMarklinMessage(m, this);
129
130        jmri.util.TimerUtil.schedule(new java.util.TimerTask() {
131            private final boolean state = newstate;
132
133            @Override
134            public void run() {
135                try {
136                    sendOffMessage((state ? 1 : 0));
137                } catch (Exception e) {
138                    log.error("Exception occurred while sending delayed off to turnout", e);
139                }
140            }
141        }, METERINTERVAL);
142    }
143
144    int getCANAddress() {
145        switch (protocol) {
146            case DCC:
147                return _number + MarklinConstants.DCCACCSTART - 1;
148            default:
149                return _number + MarklinConstants.MM1ACCSTART - 1;
150        }
151    }
152
153    // to listen for status changes from Marklin system
154    @Override
155    public void reply(MarklinReply m) {
156        if (m.getPriority() == MarklinConstants.PRIO_1 && m.getCommand() >= MarklinConstants.ACCCOMMANDSTART
157            && m.getCommand() <= MarklinConstants.ACCCOMMANDEND) {
158            if (protocol == PROTOCOL_UNKNOWN) {
159                if (m.getAddress() == _number + MarklinConstants.MM1ACCSTART - 1) {
160                    protocol = MM2;
161                } else if (m.getAddress() == _number + MarklinConstants.DCCACCSTART - 1) {
162                    protocol = DCC;
163                } else {
164                    //Message is not for us.
165                    return;
166                }
167            }
168            if (m.getAddress() == getCANAddress()) {
169                switch (m.getElement(9)) {
170                    case 0x00:
171                        setKnownStateFromCS(Turnout.THROWN);
172                        break;
173                    case 0x01:
174                        setKnownStateFromCS(Turnout.CLOSED);
175                        break;
176                    default:
177                        log.warn("Unknown state command {}", m.getElement(9));
178                }
179            }
180        }
181    }
182
183    @Override
184    public void message(MarklinMessage m) {
185        // messages are ignored
186    }
187
188    protected void sendOffMessage(int state) {
189        MarklinMessage m = MarklinMessage.getSetTurnout(getCANAddress(), state, 0x00);
190        tc.sendMarklinMessage(m, this);
191    }
192
193    static final int METERINTERVAL = 100;  // msec wait before closed
194
195    @Override
196    public void dispose() {
197        tc.removeMarklinListener(this);
198        super.dispose();
199    }
200
201    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(MarklinTurnout.class);
202
203}