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