001package jmri.jmrix.marklin;
002
003import java.util.regex.Matcher;
004import java.util.regex.Pattern;
005import jmri.Turnout;
006import jmri.implementation.AbstractTurnout;
007
008/**
009 * Implement a Turnout via Marklin communications.
010 * <p>
011 * This object doesn't listen to the Marklin communications for protocol
012 * auto-detection; protocol is determined at construction from the system name
013 * prefix. More than one Turnout object pointing to a single device is not allowed.
014 * <p>
015 * The system name encodes protocol and address:
016 * <ul>
017 *   <li>{@code MTDCC45}  — DCC accessory address 45, wire = {@code DCCACCSTART + 44}</li>
018 *   <li>{@code MTMM123}  — MM/MM2 accessory address 123, wire = {@code MM1ACCSTART + 122}</li>
019 *   <li>{@code MTCAN200} — raw CAN wire address 200, used verbatim</li>
020 *   <li>{@code MT45}     — legacy unprefixed, treated as DCC</li>
021 * </ul>
022 * <p>
023 * Based on work by Bob Jacobsen
024 *
025 * @author Kevin Dickerson Copyright (C) 2012
026 * @author Ralf Lang
027 */
028public class MarklinTurnout extends AbstractTurnout implements MarklinListener {
029
030    /** Matches the address portion after stripping system-prefix + 'T'. */
031    private static final Pattern ADDRESS_PATTERN =
032            Pattern.compile("^(MM|DCC|CAN)?(\\d+)$");
033
034    enum Protocol { MM, DCC, CAN }
035
036    private final MarklinTrafficController tc;
037    private final int _number;
038    private final Protocol protocol;
039
040    /**
041     * Create a MarklinTurnout from its full system name.
042     *
043     * @param systemName full system name, e.g. {@code MTDCC45} or {@code MT123}
044     * @param prefix     system prefix, e.g. {@code M}
045     * @param tc         traffic controller
046     */
047    public MarklinTurnout(String systemName, String prefix, MarklinTrafficController tc) {
048        super(systemName);
049        this.tc = tc;
050
051        // Strip prefix + 'T' to get the address string, e.g. "DCC45" or "123"
052        String addr = systemName.substring(prefix.length() + 1);
053        Matcher m = ADDRESS_PATTERN.matcher(addr);
054        if (!m.matches()) {
055            throw new IllegalArgumentException("Cannot parse Marklin turnout address from '" + systemName + "'");
056        }
057        String protocolStr = m.group(1); // null if unprefixed
058        _number = Integer.parseInt(m.group(2));
059        if ("MM".equals(protocolStr)) {
060            protocol = Protocol.MM;
061        } else if ("CAN".equals(protocolStr)) {
062            protocol = Protocol.CAN;
063        } else {
064            protocol = Protocol.DCC; // DCC explicit or unprefixed legacy
065        }
066
067        tc.addMarklinListener(this);
068    }
069
070    /** {@inheritDoc} */
071    @Override
072    protected void forwardCommandChangeToLayout(int newState) {
073        // implementing classes will typically have a function/listener to get
074        // updates from the layout, which will then call
075        //  public void firePropertyChange(String propertyName,
076        //          Object oldValue,
077        //          Object newValue)
078        // _once_ if anything has changed state (or set the commanded state directly)
079
080        // sort out states
081        if ((newState & Turnout.CLOSED) != 0) {
082            // first look for the double case, which we can't handle
083            if ((newState & Turnout.THROWN) != 0) {
084                // this is the disaster case!
085                log.error("Cannot command both CLOSED and THROWN {}", newState);
086            } else {
087                // send a CLOSED command
088                sendMessage(!getInverted());
089            }
090        } else {
091            // send a THROWN command
092            sendMessage(getInverted());
093        }
094    }
095
096    /**
097     * Return the wire address for the accessory command.
098     * <ul>
099     *   <li>DCC: {@code _number + DCCACCSTART - 1}</li>
100     *   <li>MM:  {@code _number + MM1ACCSTART - 1}</li>
101     *   <li>CAN: {@code _number} verbatim (raw CAN address)</li>
102     * </ul>
103     */
104    int getCANAddress() {
105        switch (protocol) {
106            case MM:  return _number + MarklinConstants.MM1ACCSTART - 1;
107            case CAN: return _number;
108            default:  return _number + MarklinConstants.DCCACCSTART - 1; // DCC
109        }
110    }
111
112    protected void sendMessage(final boolean newstate) {
113        MarklinMessage msg = MarklinMessage.getSetTurnout(getCANAddress(), (newstate ? 1 : 0), 0x01);
114        tc.sendMarklinMessage(msg, this);
115        jmri.util.TimerUtil.schedule(new java.util.TimerTask() {
116            private final boolean state = newstate;
117            @Override
118            public void run() {
119                try {
120                    sendOffMessage(state ? 1 : 0);
121                } catch (Exception e) {
122                    log.error("Exception sending delayed off to turnout", e);
123                }
124            }
125        }, METERINTERVAL);
126    }
127
128    protected void sendOffMessage(int state) {
129        MarklinMessage msg = MarklinMessage.getSetTurnout(getCANAddress(), state, 0x00);
130        tc.sendMarklinMessage(msg, this);
131    }
132
133    @Override
134    public void reply(MarklinReply m) {
135        if (m.getPriority() == MarklinConstants.PRIO_1
136                && m.getCommand() >= MarklinConstants.ACCCOMMANDSTART
137                && m.getCommand() <= MarklinConstants.ACCCOMMANDEND
138                && m.getAddress() == getCANAddress()) {
139            switch (m.getElement(9)) {
140                case 0x00: setKnownStateFromCS(Turnout.THROWN);  break;
141                case 0x01: setKnownStateFromCS(Turnout.CLOSED); break;
142                default: log.warn("Unknown state command {}", m.getElement(9));
143            }
144        }
145    }
146
147    @Override
148    public void message(MarklinMessage m) {
149    }
150
151    @Override
152    public void turnoutPushbuttonLockout(boolean b) {
153    }
154
155    /**
156     * Marklin turnouts can be inverted.
157     */
158    @Override
159    public boolean canInvert() {
160        return true;
161    }
162
163    /**
164     * Set the turnout known state to reflect what's been observed from the
165     * command station messages. A change there means that somebody commanded a
166     * state change (by using a throttle), and that command has
167     * already taken effect. Hence we use "newCommandedState" to indicate it's
168     * taken place. Must be followed by "newKnownState" to complete the turnout
169     * action.
170     *
171     * @param state Observed state, updated state from command station
172     */
173    synchronized void setCommandedStateFromCS(int state) {
174        if (getFeedbackMode() != DIRECT) return;
175        newCommandedState(state);
176    }
177
178    /**
179     * Set the turnout known state to reflect what's been observed from the
180     * command station messages. A change there means that somebody commanded a
181     * state change (by using a throttle), and that command has
182     * already taken effect. Hence we use "newKnownState" to indicate it's taken
183     * place.
184     *
185     * @param state Observed state, updated state from command station
186     */
187    synchronized void setKnownStateFromCS(int state) {
188        newCommandedState(state);
189        if (getFeedbackMode() == DIRECT) newKnownState(state);
190    }
191
192    @Override
193    public void dispose() {
194        tc.removeMarklinListener(this);
195        super.dispose();
196    }
197
198    static final int METERINTERVAL = 100; // ms between on and off pulse
199
200    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(MarklinTurnout.class);
201}