001package jmri.jmrix.lenz;
002
003import java.util.Arrays;
004import java.util.LinkedList;
005import java.util.Queue;
006import jmri.implementation.AbstractTurnout;
007import javax.annotation.concurrent.GuardedBy;
008
009/**
010 * Extend jmri.AbstractTurnout for XNet layouts
011 * <p>
012 * Turnout operation on XpressNet based systems goes through the following
013 * sequence:
014 * <ul>
015 * <li> set the commanded state, and, Send request to command station to start
016 * sending DCC operations packet to track</li>
017 * <li> Wait for response message from command station. (valid response list
018 * follows)</li>
019 * <li> Send request to command station to stop sending DCC operations packet to
020 * track</li>
021 * <li> Wait for response from command station
022 * <ul>
023 * <li>If Success Message, set Known State to Commanded State</li>
024 * <li>If error message, repeat previous step</li>
025 * </ul>
026 * </li>
027 * </ul>
028 * <p>
029 * NOTE: Some XpressNet Command stations take no action when the message
030 * generated during the third step is received.
031 * <p>
032 * Valid response messages are command station dependent, but there are 4
033 * possibilities:
034 * <ul>
035 * <li> a "Command Successfully Received..." (aka "OK") message</li>
036 * <li> a "Feedback Response Message" indicating the message is for a turnout
037 * with feedback</li>
038 * <li> a "Feedback Response Message" indicating the message is for a turnout
039 * without feedback</li>
040 * <li> The XpressNet protocol allows for no response. </li>
041 * </ul>
042 * <p>
043 * Response NOTE 1: The "Command Successfully Received..." message is generated
044 * by the lenz LIxxx interfaces when it successfully transfers the command to
045 * the command station. When this happens, the command station generates no
046 * useable response message.
047 * <p>
048 * Response NOTE 2: Currently the only command stations known to generate
049 * Feedback response messages are the Lenz LZ100 and LZV100.
050 * <p>
051 * Response NOTE 3: Software version 3.2 and above LZ100 and LZV100 may send
052 * either a Feedback response or no response at all. All other known command
053 * stations generate no response.
054 * <p>
055 * Response NOTE 4: The Feedback response messages may be generated
056 * asynchronously
057 * <p>
058 * Response NOTE 5: Feedback response messages may contain feedback for more
059 * than one device. The devices included in the response may or may not be
060 * stationary decoders (they can also be feedback encoders see
061 * {@link XNetSensor}).
062 * <p>
063 * Response NOTE 6: The last situation situation is not currently handled. The
064 * supported interfaces garantee at least an "OK" message will be sent to the
065 * computer
066 * <p>
067 * What is done with each of the response messages depends on which feedback
068 * mode is in use. "DIRECT,"MONITORING", and "EXACT" feedback mode are supported
069 * directly by this class.
070 * <p>
071 * "DIRECT" mode instantly triggers step 3 when any valid response message for
072 * this turnout is received from the command station or computer interface.
073 * <p>
074 * "SIGNAL" mode is identical to "DIRECT" mode, except it skips step 2. i.e. it
075 * triggers step 3 without receiving any reply from the command station.
076 * <p>
077 * "MONITORING" mode is an extention to direct mode. In monitoring mode, a
078 * feedback response message (for a turnout with or without feedback) is
079 * interpreted to set the known state of the turnout based on information
080 * provided by the command station.
081 * <p>
082 * "MONITORING" mode will interpret the feedback response messages when they are
083 * generated by external sources (fascia controls or other XpressNet devices)
084 * and that information is received by the computer.
085 * <p>
086 * "EXACT" mode is an extention of "MONITORING" mode. In addition to
087 * interpretting all feedback messages from the command station, "EXACT" mode
088 * will monitor the "motion complete" bit of the feedback response.
089 * <p>
090 * For turnouts without feedback, the motion complete bit is always set, so
091 * "EXACT" mode handles these messages as though the specified feedback mode is
092 * "MONITORING" mode.
093 * <p>
094 * For turnouts with feedback, "EXACT" mode polls the command station until the
095 * motion complete bit is set before triggering step 3 of the turnout operation
096 * sequence.
097 * <p>
098 * "EXACT" mode will interpret the feedback response messages when they are
099 * generated by external sources (fascia controls or other XpressNet devices)
100 * and that information is received by the computer.
101 * <p>
102 * NOTE: For LZ100 and LZV100 command stations prior to version 3.2, it may be
103 * necessary to poll for the feedback response data.
104 *
105 * @author Bob Jacobsen Copyright (C) 2001
106 * @author      Paul Bender Copyright (C) 2003-2010
107 */
108public class XNetTurnout extends AbstractTurnout implements XNetListener {
109
110    /* State information */
111    protected static final int OFFSENT = 1;
112    protected static final int COMMANDSENT = 2;
113    protected static final int STATUSREQUESTSENT = 4;
114    protected static final int QUEUEDMESSAGE = 8;
115    protected static final int IDLE = 0;
116    protected int internalState = IDLE;
117
118    /* Static arrays to hold Lenz specific feedback mode information */
119    static String[] modeNames = null;
120    static int[] modeValues = null;
121
122    @GuardedBy("this")
123    protected int _mThrown = jmri.Turnout.THROWN;
124    @GuardedBy("this")
125    protected int _mClosed = jmri.Turnout.CLOSED;
126
127    protected int mNumber;   // XpressNet turnout number
128    final XNetTurnoutStateListener _stateListener;  // Internal class object
129
130    // A queue to hold outstanding messages
131    @GuardedBy("this")
132    protected final Queue<RequestMessage> requestList;
133
134    @GuardedBy("this")
135    protected RequestMessage lastMsg = null;
136
137    protected final String _prefix; // default
138    protected final XNetTrafficController tc;
139
140    public XNetTurnout(String prefix, int pNumber, XNetTrafficController controller) {  // a human-readable turnout number must be specified!
141        super(prefix + "T" + pNumber);
142        tc = controller;
143        _prefix = prefix;
144        mNumber = pNumber;
145
146        requestList = new LinkedList<>();
147
148        /* Add additional feedback types information */
149        _validFeedbackTypes |= MONITORING | EXACT | SIGNAL;
150
151        // Default feedback mode is MONITORING
152        _activeFeedbackType = MONITORING;
153
154        setModeInformation(_validFeedbackNames, _validFeedbackModes);
155
156        // set the mode names and values based on the static values.
157        _validFeedbackNames = getModeNames();
158        _validFeedbackModes = getModeValues();
159        
160        // Register to get property change information from the superclass
161        _stateListener = new XNetTurnoutStateListener(this);
162        this.addPropertyChangeListener(_stateListener);
163        // Finally, request the current state from the layout.
164        tc.getFeedbackMessageCache().requestCachedStateFromLayout(this);
165    }
166    
167    /**
168     * Set the mode information for XpressNet Turnouts.
169     */
170    private static synchronized void setModeInformation(String[] feedbackNames, int[] feedbackModes) {
171        // if it hasn't been done already, create static arrays to hold
172        // the Lenz specific feedback information.
173        if (modeNames == null) {
174            if (feedbackNames.length != feedbackModes.length) {
175                log.error("int and string feedback arrays different length");
176            }
177            modeNames = Arrays.copyOf(feedbackNames, feedbackNames.length + 3);
178            modeValues = Arrays.copyOf(feedbackModes, feedbackNames.length + 3);
179            modeNames[feedbackNames.length] = "MONITORING";
180            modeValues[feedbackNames.length] = MONITORING;
181            modeNames[feedbackNames.length + 1] = "EXACT";
182            modeValues[feedbackNames.length + 1] = EXACT;
183            modeNames[feedbackNames.length + 2] = "SIGNAL";
184            modeValues[feedbackNames.length + 2] = SIGNAL;
185        }
186    }
187
188    static int[] getModeValues() {
189        return modeValues;
190    }
191
192    static String[] getModeNames() {
193        return modeNames;
194    }
195
196    public int getNumber() {
197        return mNumber;
198    }
199
200    /**
201     * Set the Commanded State.
202     * This method overides {@link jmri.implementation.AbstractTurnout#setCommandedState(int)}.
203     */
204    @Override
205    public void setCommandedState(int s) {
206        if (log.isDebugEnabled()) {
207            log.debug("set commanded state for XNet turnout {} to {}", getSystemName(), s);
208        }
209        synchronized (this) {
210            newCommandedState(s);
211        }
212        myOperator = getTurnoutOperator(); // MUST set myOperator before starting the thread
213        if (myOperator == null) {
214            forwardCommandChangeToLayout(s);
215            synchronized (this) {
216                // for mode ONESENSOR or TWOSENSOR CommandedState not set KnownState
217                if ( ! (getFeedbackMode() == ONESENSOR || getFeedbackMode() == TWOSENSOR)) {
218                    newKnownState(INCONSISTENT);
219                }
220            }
221        } else {
222            myOperator.start();
223        }
224    }
225
226    /**
227     * {@inheritDoc}
228     * Sends an XpressNet command.
229     */
230    @Override
231    protected synchronized void forwardCommandChangeToLayout(int s) {
232        if (s != _mClosed && s != _mThrown) {
233            log.warn("Turnout {}: state {} not forwarded to layout.", mNumber, s);
234            return;
235        }
236        // get the right packet
237        XNetMessage msg = XNetMessage.getTurnoutCommandMsg(mNumber,
238                (s & _mClosed) != 0,
239                (s & _mThrown) != 0,
240                true);
241        if (getFeedbackMode() == SIGNAL) {
242            msg.setTimeout(0); // Set the timeout to 0, so the off message can
243            // be sent immediately.
244            // leave the next line commented out for now.
245            // It may be enabled later to allow SIGNAL mode to ignore
246            // directed replies, which lets the traffic controller move on
247            // to the next message without waiting.
248            //msg.setBroadcastReply();
249            tc.sendXNetMessage(msg, null);
250            sendOffMessage();
251        } else {
252            queueMessage(msg, COMMANDSENT, this);
253        }
254    }
255
256    @Override
257    protected void turnoutPushbuttonLockout(boolean _pushButtonLockout) {
258        log.debug("Send command to {} Pushbutton {}T{}", (_pushButtonLockout ? "Lock" : "Unlock"), _prefix, mNumber);
259    }
260
261    /**
262     * Request an update on status by sending an XpressNet message.
263     */
264    @Override
265    public void requestUpdateFromLayout() {
266        // This will handle ONESENSOR and TWOSENSOR feedback modes.
267        super.requestUpdateFromLayout();
268
269        // To do this, we send an XpressNet Accessory Decoder Information
270        // Request.
271        // The generated message works for Feedback modules and turnouts
272        // with feedback, but the address passed is translated as though it
273        // is a turnout address.  As a result, we substitute our base
274        // address in for the address. after the message is returned.
275        XNetMessage msg = XNetMessage.getFeedbackRequestMsg(mNumber,
276                ((mNumber - 1) % 4) < 2);
277        queueMessage(msg,IDLE,null); //status is returned via the manager.
278
279    }
280
281    /**
282     * {@inheritDoc}
283     */
284    @Override
285    public synchronized void setInverted(boolean inverted) {
286        log.debug("Inverting Turnout State for turnout {}T{}", _prefix, mNumber);
287        if (inverted) {
288            _mThrown = jmri.Turnout.CLOSED;
289            _mClosed = jmri.Turnout.THROWN;
290        } else {
291            _mThrown = jmri.Turnout.THROWN;
292            _mClosed = jmri.Turnout.CLOSED;
293        }
294        super.setInverted(inverted);
295    }
296
297    @Override
298    public boolean canInvert() {
299        return true;
300    }
301
302    /**
303     * Package protected class which allows the Manger to send
304     * a feedback message at initialization without changing the state of the
305     * turnout with respect to whether or not a feedback request was sent. This
306     * is used only when the turnout is created by on layout feedback.
307     * @param l Message to initialize
308     */
309    synchronized void initmessage(XNetReply l) {
310        int oldState = internalState;
311        message(l);
312        internalState = oldState;
313    }
314
315    /**
316     * Handle an incoming message from the XpressNet.
317     */
318    @Override
319    public synchronized void message(XNetReply l) {
320        log.debug("received message: {}", l);
321        if (internalState == OFFSENT) {
322            if (l.isOkMessage() && !l.isUnsolicited()) {
323                /* the command was successfully received */
324                synchronized (this) {
325                    // for mode ONESENSOR or TWOSENSOR CommandedState not set KnownState
326                    if ( ! (getFeedbackMode() == ONESENSOR || getFeedbackMode() == TWOSENSOR)) {
327                        newKnownState(getCommandedState());
328                    }
329                }
330                sendQueuedMessage();
331                return;
332            } else if (l.isRetransmittableErrorMsg()) {
333                return; // don't do anything, the Traffic
334                // Controller is handling retransmitting
335                // this one.
336            } else {
337                /* Default Behavior: If anything other than an OK message
338                 is received, Send another OFF message. */
339                log.debug("Message is not OK message. Message received was: {}", l);
340                sendOffMessage();
341            }
342        }
343
344        switch (getFeedbackMode()) {
345            case EXACT:
346                handleExactModeFeedback(l);
347                break;
348            case MONITORING:
349                handleMonitoringModeFeedback(l);
350                break;
351            case ONESENSOR:
352            case TWOSENSOR:
353                handleSensorModeFeedback(l);
354                break;
355            case DIRECT:
356            default:
357                // Default is direct mode
358                handleDirectModeFeedback(l);
359        }
360    }
361
362    /**
363     * Listen for the messages to the LI100/LI101.
364     */
365    @Override
366    public synchronized void message(XNetMessage l) {
367        log.debug("received outgoing message {} for turnout {}",l,getSystemName());
368        // we want to verify this is the last message we sent
369        // so use == not .equals
370        if(lastMsg!=null && l == lastMsg.msg){
371            //if this is the last message we sent, set the state appropriately
372            internalState = lastMsg.getState();
373            // and set lastMsg to null
374            lastMsg = null;
375        }
376    }
377
378    /**
379     * Handle a timeout notification.
380     */
381    @Override
382    public synchronized void notifyTimeout(XNetMessage msg) {
383        log.debug("Notified of timeout on message {}", msg);
384        // If we're in the OFFSENT state, we need to send another OFF message.
385        if (internalState == OFFSENT) {
386            sendOffMessage();
387        }
388    }
389
390    /**
391     *  With Direct Mode feedback, if we see ANY valid response to our
392     *  request, we ask the command station to stop sending information
393     *  to the stationary decoder.
394     *  <p>
395     *  No effort is made to interpret feedback when using direct mode.
396     *
397     *  @param l an {@link XNetReply} message
398     */
399    private synchronized void handleDirectModeFeedback(XNetReply l) {
400        /* If commanded state does not equal known state, we are
401         going to check to see if one of the following conditions
402         applies:
403         1) The received message is a feedback message for a turnout
404         and one of the two addresses to which it applies is our
405         address
406         2) We receive an "OK" message, indicating the command was
407         successfully sent
408
409         If either of these two cases occur, we trigger an off message
410         */
411
412        log.debug("Handle Message for turnout {} in DIRECT feedback mode   ", mNumber);
413        if (getCommandedState() != getKnownState() || internalState == COMMANDSENT) {
414            if (l.isOkMessage()) {
415                // Finally, we may just receive an OK message.
416                log.debug("Turnout {} DIRECT feedback mode - OK message triggering OFF message.", mNumber);
417            } else {
418                // implicitly checks for isFeedbackBroadcastMessage()
419                if (!l.selectTurnoutFeedback(mNumber).isPresent()) {
420                    return;
421                }
422                log.debug("Turnout {} DIRECT feedback mode - directed reply received.", mNumber);
423            }
424            sendOffMessage();
425            // Explicitly send two off messages in Direct Mode
426            sendOffMessage();
427        }
428    }
429
430    /**
431     *  With Monitoring Mode feedback, if we see a feedback message, we
432     *  interpret that message and use it to display our feedback.
433     *  <p>
434     *  After we send a request to operate a turnout, We ask the command
435     *  station to stop sending information to the stationary decoder
436     *  when the either a feedback message or an "OK" message is received.
437     *
438     *  @param l an {@link XNetReply} message
439     */
440    private synchronized void handleMonitoringModeFeedback(XNetReply l) {
441        /* In Monitoring Mode, We have two cases to check if CommandedState
442         does not equal KnownState, otherwise, we only want to check to
443         see if the messages we receive indicate this turnout chagned
444         state
445         */
446        log.debug("Handle Message for turnout {} in MONITORING feedback mode ", mNumber);
447        if (internalState == IDLE || internalState == STATUSREQUESTSENT) {
448            if (l.onTurnoutFeedback(mNumber, this::parseFeedbackMessage)) {
449                log.debug("Turnout {} MONITORING feedback mode - state change from feedback.", mNumber);
450            }
451        } else if (getCommandedState() != getKnownState()
452                || internalState == COMMANDSENT) {
453            if (l.isOkMessage()) {
454                // Finally, we may just receive an OK message.
455                log.debug("Turnout {} MONITORING feedback mode - OK message triggering OFF message.", mNumber);
456                sendOffMessage();
457            } else {
458                // In Monitoring mode, treat both turnouts with feedback
459                // and turnouts without feedback as turnouts without
460                // feedback.  i.e. just interpret the feedback
461                // message, don't check to see if the motion is complete
462                // implicitly checks for isFeedbackBroadcastMessage()
463                if (l.onTurnoutFeedback(mNumber, this::parseFeedbackMessage)) {
464                    // We need to tell the turnout to shut off the output.
465                    log.debug("Turnout {} MONITORING feedback mode - state change from feedback, CommandedState != KnownState.", mNumber);
466                    sendOffMessage();
467                }
468            }
469        }
470    }
471
472    /**
473     *  With Exact Mode feedback, if we see a feedback message, we
474     *  interpret that message and use it to display our feedback.
475     *  <p>
476     *  After we send a request to operate a turnout, We ask the command
477     *  station to stop sending information to the stationary decoder
478     *  when the either a feedback message or an "OK" message is received.
479     *
480     *  @param reply The reply message to process
481     */
482    private synchronized void handleExactModeFeedback(XNetReply reply) {
483        // We have three cases to check if CommandedState does
484        // not equal KnownState, otherwise, we only want to check to
485        // see if the messages we receive indicate this turnout chagned
486        // state
487        log.debug("Handle Message for turnout {} in EXACT feedback mode ", mNumber);
488        if (getCommandedState() == getKnownState()
489                && (internalState == IDLE || internalState == STATUSREQUESTSENT)) {
490            // This is a feedback message, we need to check and see if it
491            // indicates this turnout is to change state or if it is for
492            // another turnout.
493            if (reply.onTurnoutFeedback(mNumber, this::parseFeedbackMessage)) {
494                log.debug("Turnout {} EXACT feedback mode - state change from feedback.", mNumber);
495            }
496        } else if (getCommandedState() != getKnownState()
497                || internalState == COMMANDSENT
498                || internalState == STATUSREQUESTSENT) {
499            if (reply.isOkMessage()) {
500                // Finally, we may just receive an OK message.
501                log.debug("Turnout {} EXACT feedback mode - OK message triggering OFF message.", mNumber);
502                sendOffMessage();
503            } else {
504                // implicitly checks for isFeedbackBroadcastMessage()
505                reply.selectTurnoutFeedback(mNumber).ifPresent(l -> {
506                    int messageType = l.getType();
507                    switch (messageType) {
508                        case 1: {
509                            // The first case is that we receive a message for
510                            // this turnout and this turnout provides feedback.
511                            // In this case, we want to check to see if the
512                            // turnout has completed its movement before doing
513                            // anything else.
514                            if (!l.isMotionComplete()) {
515                                log.debug("Turnout {} EXACT feedback mode - state change from feedback, CommandedState!=KnownState - motion not complete", mNumber);
516                                // If the motion is NOT complete, send a feedback
517                                // request for this nibble
518                                XNetMessage msg = XNetMessage.getFeedbackRequestMsg(
519                                        mNumber, ((mNumber % 4) <= 1));
520                                queueMessage(msg,STATUSREQUESTSENT ,null); //status is returned via the manager.
521                                return;
522                            } else {
523                                log.debug("Turnout {} EXACT feedback mode - state change from feedback, CommandedState!=KnownState - motion complete", mNumber);
524                            }
525                            break;
526                        }
527                        case 0: 
528                            log.debug("Turnout {} EXACT feedback mode - state change from feedback, CommandedState!=KnownState - motion complete", mNumber);
529                            // The second case is that we receive a message about
530                            // this turnout, and this turnout does not provide
531                            // feedback. In this case, we want to check the
532                            // contents of the message and act accordingly.
533                            break;
534                        default: return;
535                    }
536                    parseFeedbackMessage(l);
537                    // We need to tell the turnout to shut off the output.
538                    sendOffMessage();
539                });
540            }
541        }
542    }
543    
544    /**
545     *  Derived from handleDirectModeFeedback.
546     *  With ONESENSOR od TWOSENSOR Mode feedback, if we see ANY valid response to our
547     *  request, we ask the command station to stop sending information
548     *  to the stationary decoder.
549     *  Only one OffMessage is sent.
550     *  <p>
551     *  No effort is made to interpret feedback when using direct mode.
552     *
553     *  @param l an {@link XNetReply} message
554     */
555    private synchronized void handleSensorModeFeedback(XNetReply l) {
556        /* If no sendOff was sent we are
557         going to check to see if one of the following conditions
558         applies:
559         1) The received message is a feedback message for a turnout
560         and one of the two addresses to which it applies is our
561         address
562         2) We receive an "OK" message, indicating the command was
563         successfully sent
564
565         If either of these two cases occur, we trigger an off message
566         */
567
568        log.debug("Handle Message for turnout {} in ONESENOSR or TWOSENSOR feedback mode   ", mNumber);
569        if (internalState != OFFSENT) { 
570            if (l.isOkMessage()) {
571                // Finally, we may just receive an OK message.
572                log.debug("Turnout {} DIRECT feedback mode - OK message triggering OFF message.", mNumber);
573            } else {
574                // implicitly checks for isFeedbackBroadcastMessage()
575                if (!l.selectTurnoutFeedback(mNumber).isPresent()) {
576                    return;
577                }
578                log.debug("Turnout {} DIRECT feedback mode - directed reply received.", mNumber);
579            }
580            sendOffMessage();
581        }
582    }
583
584    /**
585     * Send an "Off" message to the decoder for this output. 
586     */
587    @SuppressWarnings("deprecation")    // The method getId() from the type Thread is deprecated since version 19
588                                        // The replacement Thread.threadId() isn't available before version 19
589    protected synchronized void sendOffMessage() {
590        // We need to tell the turnout to shut off the output.
591        if (log.isDebugEnabled()) {
592            log.debug("Sending off message for turnout {} commanded state={}", mNumber, getCommandedState());
593            log.debug("Current Thread ID: {} Thread Name {}", java.lang.Thread.currentThread().getId(), java.lang.Thread.currentThread().getName());
594        }
595        XNetMessage msg = getOffMessage();
596        lastMsg = new RequestMessage(msg,OFFSENT,this);
597        this.internalState = OFFSENT;
598        // for mode ONESENSOR or TWOSENSOR CommandedState not set KnownState
599        if ( ! (getFeedbackMode() == ONESENSOR || getFeedbackMode() == TWOSENSOR)) {
600            newKnownState(getCommandedState());
601        }
602        // Then send the message.
603        tc.sendHighPriorityXNetMessage(msg, this);
604    }
605
606    protected synchronized XNetMessage getOffMessage(){
607        return ( XNetMessage.getTurnoutCommandMsg(mNumber,
608                getCommandedState() == _mClosed,
609                getCommandedState() == _mThrown,
610                false) );
611    }
612
613    /**
614     * Parse the feedback message, and set the status of the turnout
615     * accordingly.
616     *
617     * @param l  turnout feedback item
618     * 
619     * @return 0 if address matches our turnout -1 otherwise
620     */
621    private synchronized boolean parseFeedbackMessage(FeedbackItem l) {
622        log.debug("Message for turnout {}", mNumber);
623        switch (l.getTurnoutStatus()) {
624            case THROWN:
625                newKnownState(_mThrown);
626                return true;
627            case CLOSED:
628                newKnownState(_mClosed);
629                return true;
630            default:
631                // the state is unknown or inconsistent.  If the command state
632                // does not equal the known state, and the command repeat the
633                // last command
634                if (getCommandedState() != getKnownState()) {
635                    forwardCommandChangeToLayout(getCommandedState());
636                } else {
637                    sendQueuedMessage();
638                }
639                return false;
640        }
641    }
642    
643    @Override
644    public void dispose() {
645        this.removePropertyChangeListener(_stateListener);
646        super.dispose();
647    }
648
649    /**
650     * Internal class to use for listening to state changes.
651     */
652    private static class XNetTurnoutStateListener implements java.beans.PropertyChangeListener {
653
654        final XNetTurnout _turnout;
655
656        XNetTurnoutStateListener(XNetTurnout turnout) {
657            _turnout = turnout;
658        }
659
660        /**
661         * If we're  not using DIRECT feedback mode, we need to listen for
662         * state changes to know when to send an OFF message after we set the
663         * known state.
664         * If we're using DIRECT mode, all of this is handled from the
665         * XpressNet Messages.
666         * @param event The event that causes this operation
667         */
668        @Override
669        public void propertyChange(java.beans.PropertyChangeEvent event) {
670            log.debug("propertyChange called");
671            // If we're using DIRECT feedback mode, we don't care what we see here
672            if (_turnout.getFeedbackMode() != DIRECT) {
673                if (log.isDebugEnabled()) {
674                    log.debug("propertyChange Not Direct Mode property: {} old value {} new value {}", event.getPropertyName(), event.getOldValue(), event.getNewValue());
675                }
676                if (event.getPropertyName().equals("KnownState")) {
677                    // Check to see if this is a change in the status
678                    // triggered by a device on the layout, or a change in
679                    // status we triggered.
680                    int oldKnownState = (Integer) event.getOldValue();
681                    int curKnownState = (Integer) event.getNewValue();
682                    log.debug("propertyChange KnownState - old value {} new value {}", oldKnownState, curKnownState);
683                    if (curKnownState != INCONSISTENT
684                            && _turnout.getCommandedState() == oldKnownState) {
685                        // This was triggered by feedback on the layout, change
686                        // the commanded state to reflect the new Known State
687                        if (log.isDebugEnabled()) {
688                            log.debug("propertyChange CommandedState: {}", _turnout.getCommandedState());
689                        }
690                        _turnout.newCommandedState(curKnownState);
691                    } else {
692                        // Since we always set the KnownState to
693                        // INCONSISTENT when we send a command, If the old
694                        // known state is INCONSISTENT, we just want to send
695                        // an off message
696                        if (oldKnownState == INCONSISTENT) {
697                            if (log.isDebugEnabled()) {
698                                log.debug("propertyChange CommandedState: {}", _turnout.getCommandedState());
699                            }
700                            _turnout.sendOffMessage();
701                        }
702                    }
703                }
704            }
705        }
706
707    }
708
709    /**
710     * Send message from queue.
711     */
712    protected synchronized void sendQueuedMessage() {
713
714        lastMsg = null;
715        // check to see if the queue has a message in it, and if it does,
716        // remove the first message
717        lastMsg = requestList.poll();
718        // if the queue is not empty, remove the first message
719        // from the queue, send the message, and set the state machine
720        // to the required state.
721        if (lastMsg != null) {
722            log.debug("sending message to traffic controller");
723            if(lastMsg.listener!=null) {
724                internalState = QUEUEDMESSAGE;
725            } else {
726                internalState = lastMsg.state;
727            }
728            tc.sendXNetMessage(lastMsg.getMsg(), lastMsg.getListener());
729        } else {
730            log.debug("message queue empty");
731            // if the queue is empty, set the state to idle.
732            internalState = IDLE;
733        }
734    }
735    
736    /**
737     * Queue a message.
738     * @param m Message to send
739     * @param s sequence
740     * @param l Listener to get notification of completion
741     */
742    protected synchronized void queueMessage(XNetMessage m, int s, XNetListener l) {
743        log.debug("adding message {} to message queue.  Current Internal State {}",m,internalState);
744        // put the message in the queue
745        RequestMessage msg = new RequestMessage(m, s, l);
746        // the queue is unbounded; can't throw exceptions 
747        requestList.add(msg);
748        // if the state is idle, trigger the message send
749        if (internalState == IDLE ) {
750            sendQueuedMessage();
751        }
752    }
753
754    /**
755     * Internal class to hold a request message, along with the associated throttle state.
756     */
757    protected static class RequestMessage {
758
759        private final int state;
760        private final XNetMessage msg;
761        private final XNetListener listener;
762
763        RequestMessage(XNetMessage m, int s, XNetListener listener) {
764            state = s;
765            msg = m;
766            this.listener = listener;
767        }
768
769        int getState() {
770            return state;
771        }
772
773        XNetMessage getMsg() {
774            return msg;
775        }
776
777        XNetListener getListener() {
778            return listener;
779        }
780    }
781
782    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(XNetTurnout.class);
783
784}