001package jmri.jmrix.ecos;
002
003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
004import jmri.Turnout;
005import jmri.implementation.AbstractTurnout;
006import org.slf4j.Logger;
007import org.slf4j.LoggerFactory;
008
009/**
010 * Implement a Turnout via ECoS communications.
011 * <p>
012 * This object doesn't listen to the Ecos communications. This is because it
013 * should be the only object that is sending messages for this turnout; more
014 * than one Turnout object pointing to a single device is not allowed.
015 *
016 * @author Bob Jacobsen Copyright (C) 2001
017 * @author Daniel Boudreau (C) 2007
018 */
019public class EcosTurnout extends AbstractTurnout
020        implements EcosListener {
021
022    String prefix;
023
024    int objectNumber = 0;
025    boolean masterObjectNumber = true;
026    String slaveAddress;
027    int extended = 0;
028
029    /**
030     * ECoS turnouts use the NMRA number (0-2044) as their numerical
031     * identification in the system name.
032     *
033     * @param number DCC address of the turnout
034     * @param prefix system prefix
035     * @param etc system connection traffic controller
036     * @param etm ecos turnout manager
037     */
038    public EcosTurnout(int number, String prefix, EcosTrafficController etc, EcosTurnoutManager etm) {
039        super(prefix + "T" + number);
040        _number = number;
041        /*if (_number < NmraPacket.accIdLowLimit || _number > NmraPacket.accIdHighLimit) {
042            throw new IllegalArgumentException("Turnout value: " + _number 
043                    + " not in the range " + NmraPacket.accIdLowLimit + " to " 
044                    + NmraPacket.accIdHighLimit);
045        }*/
046        this.prefix = prefix;
047        tc = etc;
048        tm = etm;
049        /* All messages from the ECoS regarding turnout status updates
050         are initally handled by the TurnoutManager, this then forwards the message
051         on to the correct Turnout */
052        
053        // update feedback modes
054        _validFeedbackTypes |= MONITORING | EXACT | INDIRECT;
055        _activeFeedbackType = MONITORING;
056
057        // if needed, create the list of feedback mode
058        // names with additional LocoNet-specific modes
059        if (modeNames == null) {
060            initFeedbackModes();
061        }
062        _validFeedbackNames = modeNames;
063        _validFeedbackModes = modeValues;
064    }
065
066    @SuppressFBWarnings(value = "ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD",
067            justification = "Only used during creation of 1st turnout")
068    private void initFeedbackModes() {
069        if (_validFeedbackNames.length != _validFeedbackModes.length) {
070            log.error("int and string feedback arrays different length");
071        }
072        String[] tempModeNames = new String[_validFeedbackNames.length + 3];
073        int[] tempModeValues = new int[_validFeedbackNames.length + 3];
074        for (int i = 0; i < _validFeedbackNames.length; i++) {
075            tempModeNames[i] = _validFeedbackNames[i];
076            tempModeValues[i] = _validFeedbackModes[i];
077        }
078        tempModeNames[_validFeedbackNames.length] = "MONITORING";
079        tempModeValues[_validFeedbackNames.length] = MONITORING;
080        tempModeNames[_validFeedbackNames.length + 1] = "INDIRECT";
081        tempModeValues[_validFeedbackNames.length + 1] = INDIRECT;
082        tempModeNames[_validFeedbackNames.length + 2] = "EXACT";
083        tempModeValues[_validFeedbackNames.length + 2] = EXACT;
084
085        modeNames = tempModeNames;
086        modeValues = tempModeValues;
087    }
088
089    static String[] modeNames = null;
090    static int[] modeValues = null;    
091    
092    EcosTrafficController tc;
093    EcosTurnoutManager tm;
094    /*Extended is used to indicate that this Ecos accessory has a secondary address assigned to it.
095     the value determines the symbol/icon used on the ecos.
096     2 - Three Way Point
097     4 - Double Slip*/
098    public static final int THREEWAY = 2;
099    public static final int DOUBLESLIP = 4;
100
101    void setExtended(int e) {
102        extended = e;
103    }
104
105    void setObjectNumber(int o) {
106        objectNumber = o;
107    }
108
109    void setSlaveAddress(int o) {
110        slaveAddress = prefix + "T" + o;
111    }
112
113    void setMasterObjectNumber(boolean o) {
114        masterObjectNumber = o;
115    }
116
117    public int getNumber() {
118        return _number;
119    }
120
121    public int getObject() {
122        return objectNumber;
123    }
124
125    public int getExtended() {
126        return extended;
127    }
128
129    public String getSlaveAddress() {
130        return slaveAddress;
131    }
132
133    /**
134     * {@inheritDoc}
135     */
136    @Override
137    protected void forwardCommandChangeToLayout(int newState) {
138        // implementing classes will typically have a function/listener to get
139        // updates from the layout, which will then call
140        //  public void firePropertyChange(String propertyName,
141        //          Object oldValue,
142        //          Object newValue)
143        // _once_ if anything has changed state (or set the commanded state directly)
144
145        // sort out states
146        if ((newState & Turnout.CLOSED) != 0) {
147            // first look for the double case, which we can't handle
148            if ((newState & Turnout.THROWN) != 0) {
149                // this is the disaster case!
150                log.error("Cannot command both CLOSED and THROWN {}", newState);
151                return;
152            } else {
153                // send a CLOSED command
154                sendMessage(true ^ getInverted());
155            }
156        } else {
157            // send a THROWN command
158            sendMessage(false ^ getInverted());
159        }
160    }
161
162    // data members
163    int _number;   // turnout number
164
165    /**
166     * Set the turnout known state to reflect what's been observed from the
167     * command station messages. A change there means that somebody commanded a
168     * state change (by using a throttle), and that command has
169     * already taken effect. Hence we use "newCommandedState" to indicate it's
170     * taken place. Must be followed by "newKnownState" to complete the turnout
171     * action.
172     *
173     * @param state Observed state, updated state from command station
174     */
175    synchronized void setCommandedStateFromCS(int state) {
176        if ((getFeedbackMode() != MONITORING)) {
177            return;
178        }
179
180        newCommandedState(state);
181    }
182
183    /**
184     * Set the turnout known state to reflect what's been observed from the
185     * command station messages. A change there means that somebody commanded a
186     * state change (by using a throttle), and that command has
187     * already taken effect. Hence we use "newKnownState" to indicate it's taken
188     * place.
189     *
190     * @param state Observed state, updated state from command station
191     */
192    synchronized void setKnownStateFromCS(int state) {
193        if ((getFeedbackMode() != MONITORING)) {
194            return;
195        }
196
197        newKnownState(state);
198    }
199
200    @Override
201    public void turnoutPushbuttonLockout(boolean b) {
202    }
203
204    /**
205     * @return ECoS turnouts can be inverted
206     */
207    @Override
208    public boolean canInvert() {
209        return true;
210    }
211
212    /**
213     * Tell the layout to go to new state.
214     *
215     * @param closed State of the turnout to be sent to the command station
216     */
217    protected void sendMessage(boolean closed) {
218        newKnownState(Turnout.UNKNOWN);
219        if (getInverted()) {
220            closed = !closed;
221        }
222        if ((masterObjectNumber) && (extended == 0)) {
223            EcosMessage m;
224            // get control
225            m = new EcosMessage("request(" + objectNumber + ", control)");
226            tc.sendEcosMessage(m, null);
227            // set state
228            m = new EcosMessage("set(" + objectNumber + ", state[" + (closed ? "0" : "1") + "])");
229            tc.sendEcosMessage(m, null);
230            // release control
231            m = new EcosMessage("release(" + objectNumber + ", control)");
232            tc.sendEcosMessage(m, null);
233        } else { //we have a 3 way or double slip!
234            //Working upon the basis that if the materObjectNumber is false than this is the second
235            //decoder address assigned, while if it is true then we are the first decoder address.
236            boolean firststate;
237            boolean secondstate;
238            if (!masterObjectNumber) {
239                //Here we are dealing with the second address
240                int turnaddr = _number - 1;
241                Turnout t = tm.getTurnout(prefix + "T" + turnaddr);
242                secondstate = closed;
243                if (t == null){
244                    log.error("Unable to locate second Turnout address {}",turnaddr);
245                    return;
246                } else {
247                    if (t.getKnownState() == CLOSED) {
248                        firststate = true;
249                    } else {
250                        firststate = false;
251                    }
252                }
253
254            } else {
255                Turnout t = tm.getTurnout(slaveAddress);
256                firststate = closed;
257                if (t == null){
258                    log.error("Unable to locate slave Turnout address {}",slaveAddress);
259                    return;
260                } else {
261
262                    if (t.getKnownState() == CLOSED) {
263                        secondstate = true;
264                    } else {
265                        secondstate = false;
266                    }
267                }
268            }
269            int setState = 0;
270            if (extended == THREEWAY) {
271                if ((firststate) && (secondstate)) {
272                    setState = 0;
273                } else if ((firststate) && (!secondstate)) {
274                    setState = 1;
275                } else {
276                    setState = 2;
277                }
278            } else if (extended == DOUBLESLIP) {
279                if ((firststate) && (secondstate)) {
280                    setState = 0;
281                } else if ((!firststate) && (!secondstate)) {
282                    setState = 1;
283                } else if ((!firststate) && (secondstate)) {
284                    setState = 2;
285                } else {
286                    setState = 3;
287                }
288            }
289
290            if (setState == 99) {
291                // log.debug("Invalid selection old state " + getKnownState() + " " + getCommandedState());
292                if (closed) {
293                    setCommandedState(THROWN);
294                } else {
295                    setCommandedState(CLOSED);
296                }
297                // log.debug("After - " + getKnownState() + " " + getCommandedState() + " " + "Is consistant " + isConsistentState());
298            } else {
299
300                EcosMessage m = new EcosMessage("request(" + objectNumber + ", control)");
301                tc.sendEcosMessage(m, this);
302                // set state
303                m = new EcosMessage("set(" + objectNumber + ", state[" + setState + "])");
304                tc.sendEcosMessage(m, this);
305                // release control
306                m = new EcosMessage("release(" + objectNumber + ", control)");
307                tc.sendEcosMessage(m, this);
308            }
309        }
310
311    }
312
313    // Listen for status changes from ECoS system.
314    int newstate = UNKNOWN;
315    int newstateext = UNKNOWN;
316
317    @Override
318    public void reply(EcosReply m) {
319
320        String msg = m.toString();
321        if (m.getResultCode() != 0) {
322            return; //The result is not valid therefore we can not set it.
323        }
324        if (m.getEcosObjectId() != objectNumber) {
325            return; //message is not for our turnout address
326        }
327        if (msg.contains("switching[0]")) {
328            log.debug("Turnout switched - new state={}", newstate);
329            /*log.debug("see new state "+newstate+" for "+_number);*/
330            //newCommandedState(newstate);
331            /*Using newKnownState, as any changes made on the ecos do not show
332              up on the panel. Therefore if an ecos route is fired the panel
333              doesn't change to reflect it.*/
334            if (extended == 0) {
335                newKnownState(newstate);
336            } else {
337                //The masterObjectNumber is used to determine if this the master or slave decoder
338                //address in an extended accessory object on the ecos.
339                if (masterObjectNumber) {
340                    newKnownState(newstate);
341                } else {
342                    newKnownState(newstateext);
343                }
344            }
345        }
346        if ((m.isUnsolicited()) || (m.getReplyType().equals("get")) || (m.getReplyType().equals("set"))) {
347            //if (msg.startsWith("<REPLY get(" + objectNumber + ",") || msg.startsWith("<EVENT " + objectNumber + ">")) {
348            int start = msg.indexOf("state[");
349            int end = msg.indexOf("]");
350            if (start > 0 && end > 0) {
351                String val = msg.substring(start + 6, end);
352                // log.debug("Extended - " + extended + " " + objectNumber);
353                if (extended == 0) {
354                    if (val.equals("0")) {
355                        newstate = CLOSED;
356                    } else if (val.equals("1")) {
357                        newstate = THROWN;
358                    } else {
359                        log.warn("val |{}| from {}", val, msg);
360                    }
361                    log.debug("newstate found: {}", newstate);
362                    if (m.getReplyType().equals("set")) {
363                       // wait to set the state until ECOS tells us to (by an event with the contents "switching[0]")
364                    } else {
365                        newKnownState(newstate);
366                    }
367                } else {
368                    if (extended == THREEWAY) { //Three way Point.
369                        if (val.equals("0")) {
370                            newstate = CLOSED;
371                            newstateext = CLOSED;
372                        } else if (val.equals("1")) {
373                            newstate = CLOSED;
374                            newstateext = THROWN;
375                        } else if (val.equals("2")) {
376                            newstate = THROWN;
377                            newstateext = CLOSED;
378                        }
379                    } else if (extended == DOUBLESLIP) { //Double Slip
380                        if (val.equals("0")) {
381                            newstate = CLOSED;
382                            newstateext = CLOSED;
383                        } else if (val.equals("1")) {
384                            newstate = THROWN;
385                            newstateext = THROWN;
386                        } else if (val.equals("2")) {
387                            newstate = THROWN;
388                            newstateext = CLOSED;
389                        } else if (val.equals("3")) {
390                            newstate = CLOSED;
391                            newstateext = THROWN;
392                        }
393                    }
394                    if (m.getReplyType().equals("set")) {
395                       // wait to set the state until ECoS tells us to (by an event with the contents "switching[0]")
396                    } else {
397                        if (masterObjectNumber) {
398                            newKnownState(newstate);
399                        } else {
400                            newKnownState(newstateext);
401                        }
402                    }
403                }
404            }
405        }
406    }
407
408    @Override
409    public void message(EcosMessage m) {
410        // messages are ignored
411    }
412
413    private final static Logger log = LoggerFactory.getLogger(EcosTurnout.class);
414
415}