001package jmri.jmrix.lenz;
002
003import java.util.Optional;
004import java.util.function.Function;
005import javax.annotation.CheckForNull;
006import javax.annotation.Nonnull;
007
008/**
009 * Represents a single response from the XpressNet.
010 *
011 * @author Paul Bender Copyright (C) 2004
012 *
013 */
014public class XNetReply extends jmri.jmrix.AbstractMRReply {
015
016    private static final String RS_TYPE = "rsType";
017    private static final String X_NET_REPLY_LI_BAUD = "XNetReplyLIBaud";
018    private static final String COLUMN_STATE = "ColumnState";
019    private static final String MAKE_LABEL = "MakeLabel";
020    private static final String POWER_STATE_ON = "PowerStateOn";
021    private static final String POWER_STATE_OFF = "PowerStateOff";
022    private static final String FUNCTION_MOMENTARY = "FunctionMomentary";
023    private static final String FUNCTION_CONTINUOUS = "FunctionContinuous";
024    private static final String BEAN_NAME_TURNOUT = "BeanNameTurnout";
025    private static final String X_NET_REPLY_NOT_OPERATED = "XNetReplyNotOperated";
026    private static final String X_NET_REPLY_THROWN_LEFT = "XNetReplyThrownLeft";
027    private static final String X_NET_REPLY_THROWN_RIGHT = "XNetReplyThrownRight";
028    private static final String X_NET_REPLY_INVALID = "XNetReplyInvalid";
029    private static final String X_NET_REPLY_CONTACT_LABEL = "XNetReplyContactLabel";
030    private static final String SPEED_STEP_MODE_X = "SpeedStepModeX";
031    // unsolicited by message type.
032
033    // Create a new reply.
034    public XNetReply() {
035        super();
036        setBinary(true);
037    }
038
039    // Create a new reply from an existing reply
040    public XNetReply(XNetReply reply) {
041        super(reply);
042        setBinary(true);
043    }
044
045    /**
046     * Create a reply from an XNetMessage.
047     * @param message existing message.
048     */
049    public XNetReply(XNetMessage message) {
050        super();
051        setBinary(true);
052        for (int i = 0; i < message.getNumDataElements(); i++) {
053            setElement(i, message.getElement(i));
054        }
055    }
056
057    /**
058     * Create a reply from a string of hex characters.
059     * @param message hex string of message.
060     */
061    public XNetReply(String message) {
062        super();
063        setBinary(true);
064        // gather bytes in result
065        byte[]  b= jmri.util.StringUtil.bytesFromHexString(message);
066        if (b.length == 0) {
067            // no such thing as a zero-length message
068            _nDataChars = 0;
069            _dataChars = null;
070            return;
071        }
072        _nDataChars = b.length;
073        _dataChars = new int[_nDataChars];
074        for (int i = 0; i < b.length; i++) {
075            setElement(i, ( b[i] & 0xff) );
076        }
077    }
078
079    /**
080     * Get the opcode as a string in hex format.
081     * @return 0x hex string of OpCode.
082     */
083    public String getOpCodeHex() {
084        return "0x" + Integer.toHexString(this.getOpCode());
085    }
086
087    /**
088     * Check whether the message has a valid parity.
089     * @return true if parity valid, else false.
090     */
091    public boolean checkParity() {
092        int len = getNumDataElements();
093        int chksum = 0x00;  /* the seed */
094
095        int loop;
096
097        for (loop = 0; loop < len - 1; loop++) {  // calculate contents for data part
098            chksum ^= getElement(loop);
099        }
100        return ((chksum & 0xFF) == getElement(len - 1));
101    }
102
103    public void setParity() {
104        int len = getNumDataElements();
105        int chksum = 0x00;  /* the seed */
106
107        int loop;
108
109        for (loop = 0; loop < len - 1; loop++) {  // calculate contents for data part
110            chksum ^= getElement(loop);
111        }
112        setElement(len - 1, chksum & 0xFF);
113    }
114
115    /**
116     * Get an integer representation of a BCD value.
117     *
118     * @param n byte in message to convert
119     * @return Integer value of BCD byte.
120     */
121    public Integer getElementBCD(int n) {
122        return Integer.decode(Integer.toHexString(getElement(n)));
123    }
124
125    /**
126     * skipPrefix is not used at this point in time, but is 
127     * defined as abstract in AbstractMRReply
128     */
129    @Override
130    protected int skipPrefix(int index) {
131        return -1;
132    }
133
134    // decode messages of a particular form
135
136    /* 
137     * The next group of routines are used by Feedback and/or turnout 
138     * control code.  These are used in multiple places within the code, 
139     * so they appear here. 
140     */
141
142    /**
143     * If this is a feedback response message for a turnout, return the address.
144     * Otherwise return -1.
145     *
146     * @return the integer address or -1 if not a turnout message
147     */
148    public int getTurnoutMsgAddr() {
149        if (this.isFeedbackMessage()) {
150            return getTurnoutAddrFromData(
151                    getElement(1),
152                    getElement(2));
153        }
154        return -1;
155    }
156    
157    private int getTurnoutAddrFromData(int a1, int a2) {
158        if (getFeedbackMessageType() > 1) {
159            return -1;
160        }
161        int address = (a1 & 0xff) * 4 + 1;
162        if ((a2 & 0x10) != 0) {
163            address += 2;
164        }
165        return address;
166    }
167
168    /**
169     * If this is a feedback broadcast message and the specified startbyte is
170     * the address byte of an addres byte data byte pair for a turnout, return
171     * the address. Otherwise return -1.
172     *
173     * @param startByte address byte of the address byte/data byte pair.
174     * @return the integer address or -1 if not a turnout message
175     */
176    public int getTurnoutMsgAddr(int startByte) {
177        if (this.isFeedbackBroadcastMessage()) {
178            int a1 = this.getElement(startByte);
179            int a2 = this.getElement(startByte + 1);
180            return getTurnoutAddrFromData(a1, a2);
181        } else {
182            return -1;
183        }
184    }
185
186    /**
187     * Parse the feedback message for a turnout, and return the status for the
188     * even or odd half of the nibble (upper or lower part).
189     *
190     * @param turnout <ul>
191     * <li>0 for the even turnout associated with the pair. This is the upper
192     * half of the data nibble asociated with the pair </li>
193     * <li>1 for the odd turnout associated with the pair. This is the lower
194     * half of the data nibble asociated with the pair </li>
195     * </ul>
196     * @return THROWN/CLOSED as defined in {@link jmri.Turnout}
197     */
198    public int getTurnoutStatus(int turnout) {
199        if (this.isFeedbackMessage() && (turnout == 0 || turnout == 1)) {
200            int a2 = this.getElement(2);
201            // fake turnout id, used just internally. Just odd/even matters.
202            return createFeedbackItem(turnout, a2).getTurnoutStatus();
203        }
204        return (-1);
205    }
206
207    /**
208     * Parse the specified address byte/data byte pair in a feedback broadcast
209     * message and see if it is for a turnout. If it is, return the status for
210     * the even or odd half of the nibble (upper or lower part)
211     *
212     * @param startByte address byte of the address byte/data byte pair.
213     * @param turnout   <ul>
214     * <li>0 for the even turnout associated with the pair. This is the upper
215     * half of the data nibble asociated with the pair </li>
216     * <li>1 for the odd turnout associated with the pair. This is the lower
217     * half of the data nibble asociated with the pair </li>
218     * </ul>
219     * @return THROWN/CLOSED as defined in {@link jmri.Turnout}
220     */
221    public int getTurnoutStatus(int startByte, int turnout) {
222        if (this.isFeedbackBroadcastMessage() && (turnout == 0 || turnout == 1)) {
223            int a2 = this.getElement(startByte + 1);
224            // fake turnout id, used just internally. Just odd/even matters.
225            return createFeedbackItem(turnout, a2).getTurnoutStatus();
226        }
227        return (-1);
228    }
229
230    /**
231     * If this is a feedback response message for a feedback encoder, return the
232     * address. Otherwise return -1.
233     *
234     * @return the integer address or -1 if not a feedback message
235     */
236    public int getFeedbackEncoderMsgAddr() {
237        if (this.isFeedbackMessage()) {
238            int a1 = this.getElement(1);
239            int messagetype = this.getFeedbackMessageType();
240            if (messagetype == 2) {
241                // This is a feedback encoder message
242                return (a1 & 0xff);
243            } else {
244                return -1;
245            }
246        } else {
247            return -1;
248        }
249    }
250
251    /**
252     * Returns the number of feedback items in the messages.
253     * For accessory info replies, always returns 1. For broadcast, it returns the
254     * number of feedback pairs. Returns 0 for non-feedback messages.
255     * 
256     * @return number of feedback pair items.
257     */
258    public final int getFeedbackMessageItems() {
259        if (isFeedbackMessage()) {
260            return 1;
261        } else if (isFeedbackBroadcastMessage()) {
262            return (this.getElement(0) & 0x0F) / 2;
263        }
264        return 0;
265    }
266
267    /**
268     * If this is a feedback broadcast message and the specified startByte is
269     * the address byte of an address byte/data byte pair for a feedback
270     * encoder, return the address. Otherwise return -1.
271     *
272     * @param startByte address byte of the address byte data byte pair.
273     * @return the integer address or -1 if not a feedback message
274     */
275    public int getFeedbackEncoderMsgAddr(int startByte) {
276        if (this.isFeedbackBroadcastMessage()) {
277            int a1 = this.getElement(startByte);
278            int messagetype = this.getFeedbackMessageType(startByte);
279            if (messagetype == 2) {
280                // This is a feedback encoder message
281                return (a1 & 0xff);
282            } else {
283                return -1;
284            }
285        } else {
286            return -1;
287        }
288    }
289
290    /**
291     * Is this a feedback response message?
292     * @return true if a feedback response, else false.
293     */
294    public boolean isFeedbackMessage() {
295        return (this.getElement(0) == XNetConstants.ACC_INFO_RESPONSE);
296    }
297
298    /**
299     * Is this a feedback broadcast message?
300     * @return true if a feedback broadcast message, else false.
301     */
302    public boolean isFeedbackBroadcastMessage() {
303        return ((this.getElement(0) & 0xF0) == XNetConstants.BC_FEEDBACK);
304    }
305
306    /**
307     * Extract the feedback message type from a feedback message this is the
308     * middle two bits of the upper byte of the second data byte.
309     *
310     * @return message type, values are:
311     * <ul>
312     * <li>0 for a turnout with no feedback</li>
313     * <li>1 for a turnout with feedback</li>
314     * <li>2 for a feedback encoder</li>
315     * <li>3 is reserved by Lenz for future use.</li>
316     * </ul>
317     */
318    public int getFeedbackMessageType() {
319        if (this.isFeedbackMessage()) {
320            int a2 = this.getElement(2);
321            return ((a2 & 0x60) / 32);
322        } else {
323            return -1;
324        }
325    }
326
327    /**
328     * Extract the feedback message type from the data byte of associated with
329     * the specified address byte specified by startByte.
330     * <p>
331     * The return value is the middle two bits of the upper byte of the data
332     * byte of an address byte/data byte pair.
333     *
334     * @param startByte The address byte for this addres byte data byte pair.
335     * @return message type, values are:
336     * <ul>
337     * <li>0 for a turnout with no feedback</li>
338     * <li>1 for a turnout with feedback</li>
339     * <li>2 for a feedback encoder</li>
340     * <li>3 is reserved by Lenz for future use.</li>
341     * </ul>
342     */
343    public int getFeedbackMessageType(int startByte) {
344        if (this.isFeedbackBroadcastMessage()) {
345            int a2 = this.getElement(startByte + 1);
346            return ((a2 & 0x60) / 32);
347        } else {
348            return -1;
349        }
350    }
351
352    public boolean isFeedbackMotionComplete(int startByte) {
353        int messageType = getFeedbackMessageType(startByte);
354        if (messageType == 1) {
355            int a2 = getElement(startByte + 1);
356            return ((a2 & 0x80) != 0x80);
357        }
358        return false;
359    }
360
361    /*
362     * Next we have a few throttle related messages
363     */
364
365    /**
366     * If this is a throttle-type message, return address.
367     * Otherwise return -1. 
368     * <p>
369     * Note we only identify the command now;
370     * the response to a request for status is not yet seen here.
371     * @return address if throttle-type message, else -1.
372     */
373    public int getThrottleMsgAddr() {
374        if (this.isThrottleMessage()) {
375            int a1 = this.getElement(2);
376            int a2 = this.getElement(3);
377            if (a1 == 0) {
378                return (a2);
379            } else {
380                return (((a1 * 256) & 0xFF00) + (a2 & 0xFF) - 0xC000);
381            }
382        } else {
383            return -1;
384        }
385    }
386
387    /**
388     * Is this a throttle message?
389     * @return true if throttle message. else false.
390     */
391    public boolean isThrottleMessage() {
392        int message = this.getElement(0);
393        return (message == XNetConstants.LOCO_INFO_NORMAL_UNIT
394                || message == XNetConstants.LOCO_INFO_RESPONSE
395                || message == XNetConstants.LOCO_INFO_MUED_UNIT
396                || message == XNetConstants.LOCO_INFO_MU_ADDRESS
397                || message == XNetConstants.LOCO_INFO_DH_UNIT
398                || message == XNetConstants.LOCO_AVAILABLE_V1
399                || message == XNetConstants.LOCO_AVAILABLE_V2
400                || message == XNetConstants.LOCO_NOT_AVAILABLE_V1
401                || message == XNetConstants.LOCO_NOT_AVAILABLE_V2);
402    }
403
404    /**
405     * Does this message indicate the locomotive has been taken over by another
406     * device?
407     * @return true if take over message, else false.
408     */
409    public boolean isThrottleTakenOverMessage() {
410        return (this.getElement(0) == XNetConstants.LOCO_INFO_RESPONSE
411                && this.getElement(1) == XNetConstants.LOCO_NOT_AVAILABLE);
412    }
413
414    /**
415     * Is this a consist message?
416     * @return true if consist message, else false.
417     */
418    public boolean isConsistMessage() {
419        int message = this.getElement(0);
420        return (message == XNetConstants.LOCO_MU_DH_ERROR
421                || message == XNetConstants.LOCO_DH_INFO_V1
422                || message == XNetConstants.LOCO_DH_INFO_V2);
423    }
424
425    /* 
426     * Finally, we have some commonly used routines that are used for 
427     * checking specific, generic, response messages.
428     */
429
430    /**
431     * In the interest of code reuse, the following function checks to see
432     * if an XpressNet Message is the OK message (01 04 05).
433     * @return true if an OK message, else false.
434     */
435    public boolean isOkMessage() {
436        return (this.getElement(0) == XNetConstants.LI_MESSAGE_RESPONSE_HEADER
437                && this.getElement(1) == XNetConstants.LI_MESSAGE_RESPONSE_SEND_SUCCESS);
438    }
439
440    /**
441     * In the interest of code reuse, the following function checks to see
442     * if an XpressNet Message is the timeslot restored message (01 07 06).
443     * @return true if a time-slot restored message.
444     */
445    public boolean isTimeSlotRestored() {
446        return (this.getElement(0) == XNetConstants.LI_MESSAGE_RESPONSE_HEADER
447                && this.getElement(1) == XNetConstants.LIUSB_TIMESLOT_RESTORED);
448    }
449
450    /**
451     * In the interest of code reuse, the following function checks to see
452     * if an XpressNet Message is the Command Station no longer providing a
453     * timeslot message (01 05 04).
454     * @return true if a time-slot revoked message, else false.
455     */
456    public boolean isTimeSlotRevoked() {
457        return (this.getElement(0) == XNetConstants.LI_MESSAGE_RESPONSE_HEADER
458                && this.getElement(1) == XNetConstants.LI_MESSAGE_RESPONSE_TIMESLOT_ERROR);
459    }
460
461    /**
462     * In the interest of code reuse, the following function checks to see
463     * if an XpressNet Message is the Command Station Busy message (61 81 e3).
464     * @return true if is a CS Busy message, else false.
465     */
466    public boolean isCSBusyMessage() {
467        return (this.getElement(0) == XNetConstants.CS_INFO
468                && this.getElement(1) == XNetConstants.CS_BUSY);
469    }
470
471
472    /**
473     * In the interest of code reuse, the following function checks to see
474     * if an XpressNet Message is the Command Station Transfer Error
475     * message (61 80 e1).
476     * @return if CS Transfer error, else false.
477     */
478    public boolean isCSTransferError() {
479        return (this.getElement(0) == XNetConstants.CS_INFO
480                && this.getElement(1) == XNetConstants.CS_TRANSFER_ERROR);
481    }
482
483    /**
484     * In the interest of code reuse, the following function checks to see
485     * if an XpressNet Message is the not supported Error
486     * message (61 82 e3).
487     * @return true if unsupported error, else false.
488     */
489    public boolean isUnsupportedError() {
490        return (this.getElement(0) == XNetConstants.CS_INFO
491                && this.getElement(1) == XNetConstants.CS_NOT_SUPPORTED);
492    }
493
494    /**
495     * In the interest of code reuse, the following function checks to see
496     * if an XpressNet Message is a communications error message.
497     * <p>
498     * The errors handled are:
499     *  01 01 00  -- Error between interface and the PC
500     *  01 02 03  -- Error between interface and the Command Station
501     *  01 03 02  -- Unknown Communications Error
502     *  01 06 07  -- LI10x Buffer Overflow
503     *  01 0A 0B  -- LIUSB only. Request resend of data.
504     * @return true if comm error message, else false.
505     */
506    public boolean isCommErrorMessage() {
507        return (this.getElement(0) == XNetConstants.LI_MESSAGE_RESPONSE_HEADER
508                && (this.getElement(1) == XNetConstants.LI_MESSAGE_RESPONSE_UNKNOWN_DATA_ERROR
509                || this.getElement(1) == XNetConstants.LI_MESSAGE_RESPONSE_CS_DATA_ERROR
510                || this.getElement(1) == XNetConstants.LI_MESSAGE_RESPONSE_PC_DATA_ERROR
511                || this.getElement(1) == XNetConstants.LIUSB_RETRANSMIT_REQUEST
512                || this.getElement(1) == XNetConstants.LI_MESSAGE_RESPONSE_BUFFER_OVERFLOW)
513                || this.isTimeSlotErrorMessage());
514    }
515
516    /**
517     * In the interest of code reuse, the following function checks to see
518     * if an XpressNet Message is a communications error message.
519     * <p>
520     * The errors handled are:
521     *  01 05 04  -- Timeslot Error
522     *  01 07 06  -- Timeslot Restored
523     *  01 08 09  -- Data sent while there is no Timeslot
524     * @return true if time slot error, else false.
525     */
526    public boolean isTimeSlotErrorMessage() {
527        return (this.getElement(0) == XNetConstants.LI_MESSAGE_RESPONSE_HEADER
528                && (this.getElement(1) == XNetConstants.LIUSB_REQUEST_SENT_WHILE_NO_TIMESLOT
529                || this.getElement(1) == XNetConstants.LIUSB_TIMESLOT_RESTORED
530                || this.getElement(1) == XNetConstants.LI_MESSAGE_RESPONSE_TIMESLOT_ERROR));
531    }
532
533
534    /**
535     * Is this message a service mode response?
536     * @return true if a service mode response, else false.
537     */
538    public boolean isServiceModeResponse() {
539        return (getElement(0) == XNetConstants.CS_SERVICE_MODE_RESPONSE
540                && (getElement(1) == XNetConstants.CS_SERVICE_DIRECT_RESPONSE
541                || getElement(1) == (XNetConstants.CS_SERVICE_DIRECT_RESPONSE + 1)
542                || getElement(1) == (XNetConstants.CS_SERVICE_DIRECT_RESPONSE + 2)
543                || getElement(1) == (XNetConstants.CS_SERVICE_DIRECT_RESPONSE + 3)
544                || getElement(1) == XNetConstants.CS_SERVICE_REG_PAGE_RESPONSE));
545    }
546
547    /**
548     * Is this message a register or paged mode programming response?
549     * @return true if register or paged mode programming response, else false.
550     */
551    public boolean isPagedModeResponse() {
552        return (getElement(0) == XNetConstants.CS_SERVICE_MODE_RESPONSE
553                && getElement(1) == XNetConstants.CS_SERVICE_REG_PAGE_RESPONSE);
554    }
555
556    /**
557     * Is this message a direct CV mode programming response?
558     * @return true if direct CV mode programming response, else false.
559     */
560    public boolean isDirectModeResponse() {
561        return (getElement(0) == XNetConstants.CS_SERVICE_MODE_RESPONSE
562                && (getElement(1) == XNetConstants.CS_SERVICE_DIRECT_RESPONSE
563                || getElement(1) == (XNetConstants.CS_SERVICE_DIRECT_RESPONSE + 1)
564                || getElement(1) == (XNetConstants.CS_SERVICE_DIRECT_RESPONSE + 2)
565                || getElement(1) == (XNetConstants.CS_SERVICE_DIRECT_RESPONSE + 3)));
566    }
567
568    /**
569     * @return the CV value associated with a service mode reply
570     * return -1 if not a service mode message.
571     */
572    public int getServiceModeCVNumber() {
573        int cv = -1;
574        if (isServiceModeResponse()) {
575            if ((getElement(1) & XNetConstants.CS_SERVICE_DIRECT_RESPONSE) == XNetConstants.CS_SERVICE_DIRECT_RESPONSE) {
576                cv = (getElement(1) - XNetConstants.CS_SERVICE_DIRECT_RESPONSE) * 256 + getElement(2);
577            } else {
578                cv = getElement(2);
579            }
580        }
581        return (cv);
582    }
583
584    /**
585     * @return the value returned by the DCC system associated with a 
586     * service mode reply.
587     * return -1 if not a service mode message.
588     */
589    public int getServiceModeCVValue() {
590        int value = -1;
591        if (isServiceModeResponse()) {
592            value = getElement(3);
593        }
594        return (value);
595    }
596
597    /**
598     * @return true if the message is an error message indicating
599     * we should retransmit.
600     */
601    @Override
602    public boolean isRetransmittableErrorMsg() {
603        return (this.isCSBusyMessage()
604                || this.isCommErrorMessage()
605                || this.isCSTransferError());
606    }
607
608    /**
609     * @return true if the message is an unsolicited message
610     */
611    @Override
612    public boolean isUnsolicited() {
613        // The message may be set as an unsolicited message else where
614        // or it may be classified as unsolicited based on the type of 
615        // message received.
616        // NOTE: The feedback messages may be received in either solicited
617        // or unsolicited form.  requesting code can mark the reply as solicited
618        // by calling the resetUnsolicited function.
619        return (super.isUnsolicited()
620                || this.isThrottleTakenOverMessage());
621    }
622
623    /**
624     * Resets the unsolicited feedback flag. If the reply was not a feedback,
625     * or was received as a broadcast - unsolicited from the command station, 
626     * this method  <b>will not cause</b> the {@link #isUnsolicited()} to 
627     * return {@code false}.  
628     * <p>
629     * Messages sent as unsolicited by the command station can not be turned 
630     * to solicited.
631     * @deprecated since 4.21.1 without replacement
632     */
633    @Deprecated
634    public final void resetUnsolicited() {
635        // method deprecated
636    }
637    
638    /**
639     * Mask to identify a turnout feedback + correct nibble. Turnout types differ in
640     * 6th bit, so it's left out (is variable).
641     */
642    private static final int FEEDBACK_TURNOUT_MASK = 0b0101_0000;
643    
644    /**
645     * Mask to identify a feedback module + correct nibble. Turnout modules have
646     * type exactly 2.
647     */
648    private static final int FEEDBACK_MODULE_MASK  = 0b0111_0000;
649    
650    /**
651     * The value of "feedback module" type. 
652     */
653    private static final int FEEDBACK_TYPE_FBMODULE = 0b0100_0000;
654    
655    /**
656     * Bit that indicates the higher nibble in module or turnout feedback
657     */
658    private static final int FEEDBACK_HIGH_NIBBLE = 0b0001_0000;
659    
660    private int findFeedbackData(int baseAddress, int selector, int mask) {
661        if (isFeedbackMessage()) {
662            // shorctcut for single-item msg
663            int data = getElement(2);
664            if (getElement(1) == baseAddress &&
665                (data & mask) == selector) {
666                return data;
667            }
668        } else {
669            int start = 1;
670            for (int cnt = getFeedbackMessageItems(); cnt > 0; cnt--, start += 2) {
671                int data = getElement(start + 1);
672                if (getElement(start) == baseAddress &&
673                    (data & mask) == selector) {
674                    return data;
675                }
676            }
677        }
678        return -1;
679    }
680
681    /**
682     * Returns value of the given feedback module bit. Returns {@link Optional}
683     * that is non-empty, if the feedback was present. The Optional's value indicates the
684     * feedback state.
685     * 
686     * @param sensorNumber the sensor bit ID
687     * @return optional sensor state.
688     */
689    @CheckForNull
690    public Boolean selectModuleFeedback(int sensorNumber) {
691        if (!isFeedbackBroadcastMessage() || sensorNumber == 0 || sensorNumber >= 1024) {
692            return null;
693        }
694        // feedback address directly addresses 8-bit module, XpressNet spec 3.0:2.1.11.
695        int s = sensorNumber - 1;
696        int baseAddress = (s / 8);
697        int selector2 = (s & 0x04) != 0 ? 
698                FEEDBACK_TYPE_FBMODULE | FEEDBACK_HIGH_NIBBLE : 
699                FEEDBACK_TYPE_FBMODULE;
700        int res = findFeedbackData(baseAddress, selector2, FEEDBACK_MODULE_MASK);
701        return res == -1 ? null : 
702                (res & (1 << (s % 4))) > 0;
703    }
704    
705    /**
706     * Calls processor for turnout's feedback, returns the processor's outcome.
707     * Searches for the turnout feedback for the given accessory. If found,
708     * runs a processor on the feedback item, and returns its Boolean result.
709     * <p>
710     * Returns {@code false}, if matching feedback is not found.
711     * @param accessoryNumber the turnout number
712     * @param proc the processor
713     * @return {@code false} if feedback was not found, or a result of {@code proc()}.
714     */
715    public boolean onTurnoutFeedback(int accessoryNumber, Function<FeedbackItem, Boolean> proc) {
716        return selectTurnoutFeedback(accessoryNumber).map(proc).orElse(false);
717    }
718
719    /**
720     * Selects a matching turnout feedback. Finds turnout feedback for the given {@code accessoryNumber}.
721     * Returns an encapsulated feedback, that can be inspected. If no matching feedback is
722     * present, returns empty {@link Optional}.
723     * @param accessoryNumber the turnout number
724     * @return optional feedback item.
725     */
726    @Nonnull
727    public Optional<FeedbackItem> selectTurnoutFeedback(int accessoryNumber) {
728        // shortcut for single-item messages.
729        if (!isFeedbackBroadcastMessage() || accessoryNumber <= 0 || accessoryNumber >= 1024) {
730            return Optional.empty();
731        }
732        int a = accessoryNumber - 1;
733        int base = (a / 4);
734        // the mask makes the turnout feedback type bit irrelevant
735        int selector2 = (a & 0x02) != 0 ? FEEDBACK_HIGH_NIBBLE : 0;
736        int r = findFeedbackData(base, selector2, FEEDBACK_TURNOUT_MASK);
737        if (r == -1) {
738            return Optional.empty();
739        }
740        FeedbackItem item = new FeedbackItem(this, accessoryNumber, r);
741        return Optional.of(item);
742    }
743    
744    protected final FeedbackItem createFeedbackItem(int n, int d) {
745        return new FeedbackItem(this, n, d);
746    }
747
748    /**
749     * @return a string representation of the reply suitable for display in the
750     * XpressNet monitor.
751     */
752    @Override
753    public String toMonitorString(){
754        StringBuilder text;
755        // First, Decode anything that is sent by the LI10x, and
756        // not the command station
757        
758        if(getElement(0) == XNetConstants.LI_MESSAGE_RESPONSE_HEADER){
759            switch(this.getElement(1)) {
760              case XNetConstants.LI_MESSAGE_RESPONSE_PC_DATA_ERROR:
761                 text = new StringBuilder(Bundle.getMessage("XNetReplyErrorPCtoLI"));
762                 break;
763              case XNetConstants.LI_MESSAGE_RESPONSE_CS_DATA_ERROR:
764                 text = new StringBuilder(Bundle.getMessage("XNetReplyErrorLItoCS"));
765                 break;
766              case XNetConstants.LI_MESSAGE_RESPONSE_UNKNOWN_DATA_ERROR:
767                 text = new StringBuilder(Bundle.getMessage("XNetReplyErrorUnknown"));
768                 break;
769              case XNetConstants.LI_MESSAGE_RESPONSE_SEND_SUCCESS:
770                 text = new StringBuilder(Bundle.getMessage("XNetReplyOkMessage"));
771                 break;
772              case XNetConstants.LI_MESSAGE_RESPONSE_TIMESLOT_ERROR:
773                 text = new StringBuilder(Bundle.getMessage("XNetReplyErrorNoTimeSlot"));
774                 break;
775              case XNetConstants.LI_MESSAGE_RESPONSE_BUFFER_OVERFLOW:
776                 text = new StringBuilder(Bundle.getMessage("XNetReplyErrorBufferOverflow"));
777                 break;
778              case XNetConstants.LIUSB_TIMESLOT_RESTORED:
779                 text = new StringBuilder(Bundle.getMessage("XNetReplyTimeSlotRestored"));
780                 break;
781              case XNetConstants.LIUSB_REQUEST_SENT_WHILE_NO_TIMESLOT:
782                 text = new StringBuilder(Bundle.getMessage("XNetReplyRequestSentWhileNoTimeslot"));
783                 break;
784              case XNetConstants.LIUSB_BAD_DATA_IN_REQUEST:
785                 text = new StringBuilder(Bundle.getMessage("XNetReplyBadDataInRequest"));
786                 break;
787              case XNetConstants.LIUSB_RETRANSMIT_REQUEST:
788                 text = new StringBuilder(Bundle.getMessage("XNetReplyRetransmitRequest"));
789                 break;
790              default:
791                 text = new StringBuilder(toString());
792           }
793        } else if (getElement(0) == XNetConstants.LI_VERSION_RESPONSE) {
794            text = new StringBuilder(Bundle.getMessage("XNetReplyLIVersion", (getElementBCD(1).floatValue()) / 10, (getElementBCD(2).floatValue()) / 10));
795        } else if (getElement(0) == XNetConstants.LI101_REQUEST) {
796            // The request and response for baud rate look the same,
797            // so we need this for both incoming and outgoing directions
798            switch (getElement(1)) {
799                case XNetConstants.LI101_REQUEST_ADDRESS:
800                    text = new StringBuilder(Bundle.getMessage("XNetReplyLIAddress", getElement(2)));
801                    break;
802                case XNetConstants.LI101_REQUEST_BAUD:
803                    switch (getElement(2)) {
804                        case 1:
805                            text = new StringBuilder(Bundle.getMessage(X_NET_REPLY_LI_BAUD, Bundle.getMessage("LIBaud19200")));
806                            break;
807                        case 2:
808                            text = new StringBuilder(Bundle.getMessage(X_NET_REPLY_LI_BAUD, Bundle.getMessage("Baud38400")));
809                            break;
810                        case 3:
811                            text = new StringBuilder(Bundle.getMessage(X_NET_REPLY_LI_BAUD, Bundle.getMessage("Baud57600")));
812                            break;
813                        case 4:
814                            text = new StringBuilder(Bundle.getMessage(X_NET_REPLY_LI_BAUD, Bundle.getMessage("Baud115200")));
815                            break;
816                        default:
817                            text = new StringBuilder(Bundle.getMessage(X_NET_REPLY_LI_BAUD, Bundle.getMessage("BaudOther")));
818                    }
819                    break;
820                default:
821                    text = new StringBuilder(toString());
822            }
823            /* Next, check the "CS Info" messages */
824        } else if (getElement(0) == XNetConstants.CS_INFO) {
825            switch (getElement(1)) {
826                case XNetConstants.BC_NORMAL_OPERATIONS:
827                    text = new StringBuilder(Bundle.getMessage("XNetReplyBCNormalOpsResumed"));
828                    break;
829                case XNetConstants.BC_EVERYTHING_OFF:
830                    text = new StringBuilder(Bundle.getMessage("XNetReplyBCEverythingOff"));
831                    break;
832                case XNetConstants.BC_SERVICE_MODE_ENTRY:
833                    text = new StringBuilder(Bundle.getMessage("XNetReplyBCServiceEntry"));
834                    break;
835                case XNetConstants.PROG_SHORT_CIRCUIT:
836                    text = new StringBuilder(Bundle.getMessage("XNetReplyServiceModeShort"));
837                    break;
838                case XNetConstants.PROG_BYTE_NOT_FOUND:
839                    text = new StringBuilder(Bundle.getMessage("XNetReplyServiceModeDataByteNotFound"));
840                    break;
841                case XNetConstants.PROG_CS_BUSY:
842                    text = new StringBuilder(Bundle.getMessage("XNetReplyServiceModeCSBusy"));
843                    break;
844                case XNetConstants.PROG_CS_READY:
845                    text = new StringBuilder(Bundle.getMessage("XNetReplyServiceModeCSReady"));
846                    break;
847                case XNetConstants.CS_BUSY:
848                    text = new StringBuilder(Bundle.getMessage("XNetReplyCSBusy"));
849                    break;
850                case XNetConstants.CS_NOT_SUPPORTED:
851                    text = new StringBuilder(Bundle.getMessage("XNetReplyCSNotSupported"));
852                    break;
853                case XNetConstants.CS_TRANSFER_ERROR:
854                    text = new StringBuilder(Bundle.getMessage("XNetReplyCSTransferError"));
855                    break;
856                /* The remaining cases are for a Double Header or MU Error */
857                case XNetConstants.CS_DH_ERROR_NON_OP:
858                    text = new StringBuilder(Bundle.getMessage("XNetReplyV1DHErrorNotOperated"));
859                    break;
860                case XNetConstants.CS_DH_ERROR_IN_USE:
861                    text = new StringBuilder(Bundle.getMessage("XNetReplyV1DHErrorInUse"));
862                    break;
863                case XNetConstants.CS_DH_ERROR_ALREADY_DH:
864                    text = new StringBuilder(Bundle.getMessage("XNetReplyV1DHErrorAlreadyDH"));
865                    break;
866                case XNetConstants.CS_DH_ERROR_NONZERO_SPD:
867                    text = new StringBuilder(Bundle.getMessage("XNetReplyV1DHErrorNonZeroSpeed"));
868                    break;
869                default:
870                    text = new StringBuilder(toString());
871            }
872        } else if (getElement(0) == XNetConstants.BC_EMERGENCY_STOP
873                && getElement(1) == XNetConstants.BC_EVERYTHING_STOP) {
874            text = new StringBuilder(Bundle.getMessage("XNetReplyBCEverythingStop"));
875            /* Followed by Service Mode responses */
876        } else if (getElement(0) == XNetConstants.CS_SERVICE_MODE_RESPONSE) {
877            if (isDirectModeResponse()) {
878                text = new StringBuilder(Bundle.getMessage("XNetReplyServiceModeDirectResponse", getServiceModeCVNumber(), getServiceModeCVValue()));
879            } else if (isPagedModeResponse()) {
880                text = new StringBuilder(Bundle.getMessage("XNetReplyServiceModePagedResponse", getServiceModeCVNumber(), getServiceModeCVValue()));
881            } else if (getElement(1) == XNetConstants.CS_SOFTWARE_VERSION) {
882                String typeString; 
883                switch (getElement(3)) {
884                    case 0x00:
885                        typeString = Bundle.getMessage("CSTypeLZ100");
886                        break;
887                    case 0x01:
888                        typeString = Bundle.getMessage("CSTypeLH200");
889                        break;
890                    case 0x02:
891                        typeString = Bundle.getMessage("CSTypeCompact");
892                        break;
893                    // GT 2007/11/6 - Added multiMaus
894                    case 0x10:
895                        typeString = Bundle.getMessage("CSTypeMultiMaus");
896                        break;
897                    default:
898                        typeString = "" + getElement(3);
899                }
900                text = new StringBuilder(Bundle.getMessage("XNetReplyCSVersion", (getElementBCD(2).floatValue()) / 10, typeString));
901            } else {
902                text = new StringBuilder(toString());
903            }
904 /* We want to look at responses to specific requests made to the Command Station */
905        } else if (getElement(0) == XNetConstants.CS_REQUEST_RESPONSE) {
906            if (getElement(1) == XNetConstants.CS_STATUS_RESPONSE) {
907                text = new StringBuilder(Bundle.getMessage("XNetReplyCSStatus") + " ");
908                int statusByte = getElement(2);
909                if ((statusByte & 0x01) == 0x01) {
910                    // Command station is in Emergency Off Mode
911                    text.append(Bundle.getMessage("XNetCSStatusEmergencyOff")).append("; ");
912                }
913                if ((statusByte & 0x02) == 0x02) {
914                    // Command station is in Emergency Stop Mode
915                    text.append(Bundle.getMessage("XNetCSStatusEmergencyStop")).append("; ");
916                }
917                if ((statusByte & 0x08) == 0x08) {
918                    // Command station is in Service Mode
919                    text.append(Bundle.getMessage("XNetCSStatusServiceMode")).append("; ");
920                }
921                if ((statusByte & 0x40) == 0x40) {
922                    // Command station is in Power Up Mode
923                    text.append(Bundle.getMessage("XNetCSStatusPoweringUp")).append("; ");
924                }
925                if ((statusByte & 0x04) == 0x04) {
926                    text.append(Bundle.getMessage("XNetCSStatusPowerModeAuto")).append("; ");
927                } else {
928                    text.append(Bundle.getMessage("XNetCSStatusPowerModeManual")).append("; ");
929                }
930                if ((statusByte & 0x80) == 0x80) {
931                    // Command station has a experienced a ram check error
932                    text.append(Bundle.getMessage("XNetCSStatusRamCheck"));
933                }
934            } else if (getElement(1) == XNetConstants.CS_SOFTWARE_VERSION) {
935                /* This is a Software version response for XpressNet
936                 Version 1 or 2 */
937                text = new StringBuilder(Bundle.getMessage("XNetReplyCSVersionV1", (getElementBCD(2).floatValue()) / 10));
938            } else {
939                text = new StringBuilder(toString());
940            }
941
942            // MU and Double Header Related Responses
943        } else if (getElement(0) == XNetConstants.LOCO_MU_DH_ERROR) {
944            switch (getElement(1)) {
945                case 0x81:
946                    text = new StringBuilder(Bundle.getMessage("XNetReplyDHErrorNotOperated"));
947                    break;
948                case 0x82:
949                    text = new StringBuilder(Bundle.getMessage("XNetReplyDHErrorInUse"));
950                    break;
951                case 0x83:
952                    text = new StringBuilder(Bundle.getMessage("XNetReplyDHErrorAlreadyDH"));
953                    break;
954                case 0x84:
955                    text = new StringBuilder(Bundle.getMessage("XNetReplyDHErrorNonZeroSpeed"));
956                    break;
957                case 0x85:
958                    text = new StringBuilder(Bundle.getMessage("XNetReplyDHErrorLocoNotMU"));
959                    break;
960                case 0x86:
961                    text = new StringBuilder(Bundle.getMessage("XNetReplyDHErrorLocoNotMUBase"));
962                    break;
963                case 0x87:
964                    text = new StringBuilder(Bundle.getMessage("XNetReplyDHErrorCanNotDelete"));
965                    break;
966                case 0x88:
967                    text = new StringBuilder(Bundle.getMessage("XNetReplyDHErrorStackFull"));
968                    break;
969                default:
970                    text = new StringBuilder(Bundle.getMessage("XNetReplyDHErrorOther", (getElement(1) - 0x80)));
971            }
972            // Loco Information Response Messages
973        } else if (getElement(0) == XNetConstants.LOCO_INFO_NORMAL_UNIT) {
974            if (getElement(1) == XNetConstants.LOCO_FUNCTION_STATUS_HIGH_MOM) {
975                text = new StringBuilder(Bundle.getMessage("XNetReplyLocoStatus13Label") + " ");
976                // message byte 3, contains F20,F19,F18,F17,F16,F15,F14,F13
977                int element3 = getElement(2);
978                // message byte 4, contains F28,F27,F26,F25,F24,F23,F22,F21
979                int element4 = getElement(3);
980                text.append(parseFunctionHighMomentaryStatus(element3, element4));
981            } else {
982                text = new StringBuilder(Bundle.getMessage("XNetReplyLocoNormalLabel") + ",");
983                text.append(parseSpeedAndDirection(getElement(1), getElement(2))).append(" ");
984                // message byte 4, contains F0,F1,F2,F3,F4
985                int element3 = getElement(3);
986                // message byte 5, contains F12,F11,F10,F9,F8,F7,F6,F5
987                int element4 = getElement(4);
988                text.append(parseFunctionStatus(element3, element4));
989            }
990        } else if (getElement(0) == XNetConstants.LOCO_INFO_MUED_UNIT) {
991            if (getElement(1) == 0xF8) {
992                // This message is a Hornby addition to the protocol
993                // indicating the speed and direction of a locomoitve
994                // controlled by the elite's built in throttles
995                text = new StringBuilder(Bundle.getMessage("XNetReplyLocoEliteSLabel") + " ");
996                text.append(LenzCommandStation.calcLocoAddress(getElement(2), getElement(3)));
997                text.append(",").append(parseSpeedAndDirection(getElement(4), getElement(5))).append(" ");
998            } else if (getElement(1) == 0xF9) {
999                // This message is a Hornby addition to the protocol
1000                // indicating the function on/off status of a locomoitve
1001                // controlled by the elite's built in throttles
1002                text = new StringBuilder(Bundle.getMessage("XNetReplyLocoEliteFLabel") + " ");
1003                text.append(LenzCommandStation.calcLocoAddress(getElement(2), getElement(3))).append(" ");
1004                // message byte 5, contains F0,F1,F2,F3,F4
1005                int element4 = getElement(4);
1006                // message byte 5, contains F12,F11,F10,F9,F8,F7,F6,F5
1007                int element5 = getElement(5);
1008                text.append(parseFunctionStatus(element4, element5));
1009            } else {
1010                text = new StringBuilder(Bundle.getMessage("XNetReplyLocoMULabel") + ",");
1011                text.append(parseSpeedAndDirection(getElement(1), getElement(2)));
1012                // message byte 4, contains F0,F1,F2,F3,F4
1013                int element3 = getElement(3);
1014                // message byte 5, contains F12,F11,F10,F9,F8,F7,F6,F5
1015                int element4 = getElement(4);
1016                text.append(parseFunctionStatus(element3, element4));
1017            }
1018        } else if (getElement(0) == XNetConstants.LOCO_INFO_MU_ADDRESS) {
1019            text = new StringBuilder(Bundle.getMessage("XNetReplyLocoMUBaseLabel") + ",");
1020            text.append(parseSpeedAndDirection(getElement(1), getElement(2))).append(" ");
1021        } else if (getElement(0) == XNetConstants.LOCO_INFO_DH_UNIT) {
1022            text = new StringBuilder(Bundle.getMessage("XNetReplyLocoDHLabel") + ",");
1023            text.append(parseSpeedAndDirection(getElement(1), getElement(2))).append(" ");
1024            // message byte 4, contains F0,F1,F2,F3,F4
1025            int element3 = getElement(3);
1026            // message byte 5, contains F12,F11,F10,F9,F8,F7,F6,F5
1027            int element4 = getElement(4);
1028            text.append(parseFunctionStatus(element3, element4));
1029            text.append(" ").append(Bundle.getMessage("XNetReplyLoco2DHLabel")).append(" ");
1030            text.append(LenzCommandStation.calcLocoAddress(getElement(5), getElement(6)));
1031        } else if (getElement(0) == XNetConstants.LOCO_INFO_RESPONSE) {
1032            text = new StringBuilder(Bundle.getMessage("XNetReplyLocoLabel") + " ");
1033            switch (getElement(1)) {
1034                case XNetConstants.LOCO_SEARCH_RESPONSE_N:
1035                    text.append(Bundle.getMessage("XNetReplySearchNormalLabel")).append(" ");
1036                    text.append(getThrottleMsgAddr());
1037                    break;
1038                case XNetConstants.LOCO_SEARCH_RESPONSE_DH:
1039                    text.append(Bundle.getMessage("XNetReplySearchDHLabel")).append(" ");
1040                    text.append(getThrottleMsgAddr());
1041                    break;
1042                case XNetConstants.LOCO_SEARCH_RESPONSE_MU_BASE:
1043                    text.append(Bundle.getMessage("XNetReplySearchMUBaseLabel")).append(" ");
1044                    text.append(getThrottleMsgAddr());
1045                    break;
1046                case XNetConstants.LOCO_SEARCH_RESPONSE_MU:
1047                    text.append(Bundle.getMessage("XNetReplySearchMULabel")).append(" ");
1048                    text.append(getThrottleMsgAddr());
1049                    break;
1050                case XNetConstants.LOCO_SEARCH_NO_RESULT:
1051                    text.append(Bundle.getMessage("XNetReplySearchFailedLabel")).append(" ");
1052                    text.append(getThrottleMsgAddr());
1053                    break;
1054                case XNetConstants.LOCO_NOT_AVAILABLE:
1055                    text.append(Bundle.getMessage(RS_TYPE)).append(" ");
1056                    text.append(getThrottleMsgAddr()).append(" ");
1057                    text.append(Bundle.getMessage("XNetReplyLocoOperated"));
1058                    break;
1059                case XNetConstants.LOCO_FUNCTION_STATUS:
1060                    locoFunctionStatusText(text);
1061                    break;
1062                case XNetConstants.LOCO_FUNCTION_STATUS_HIGH:
1063                    locoFunctionStatusHighText(text);
1064                    break;
1065                default:
1066                    text = new StringBuilder(toString());
1067            }
1068            // Feedback Response Messages
1069        } else if (isFeedbackBroadcastMessage()) {
1070            text = new StringBuilder().append(Bundle.getMessage("XNetReplyFeedbackLabel")).append(" ");
1071            int numDataBytes = getElement(0) & 0x0f;
1072            for (int i = 1; i < numDataBytes; i += 2) {
1073                switch (getFeedbackMessageType(i)) {
1074                    case 0:
1075                        text.append(getTurnoutReplyMonitorString(i, "TurnoutWoFeedback"));
1076                        break;
1077                    case 1:
1078                        text.append(getTurnoutReplyMonitorString(i, "TurnoutWFeedback"));
1079                        break;
1080                    case 2:
1081                        text.append(Bundle.getMessage("XNetReplyFeedbackEncoder")).append(" ").append(getFeedbackEncoderMsgAddr(i) + 1);
1082                        boolean highnibble = ((getElement(i + 1) & 0x10) == 0x10);
1083                        text.append(" ").append(Bundle.getMessage(X_NET_REPLY_CONTACT_LABEL)).append(" ").append(highnibble ? 5 : 1);
1084
1085                        text.append(" ").append(Bundle.getMessage(MAKE_LABEL, Bundle.getMessage(COLUMN_STATE))).append(" ")
1086                                .append(((getElement(i + 1) & 0x01) == 0x01) ? Bundle.getMessage(POWER_STATE_ON) : Bundle.getMessage(POWER_STATE_OFF));
1087                        text.append("; ").append(Bundle.getMessage(X_NET_REPLY_CONTACT_LABEL)).append(" ").append(highnibble ? 6 : 2);
1088
1089                        text.append(" ").append(Bundle.getMessage(MAKE_LABEL, Bundle.getMessage(COLUMN_STATE))).append(" ")
1090                                .append(((getElement(i + 1) & 0x02) == 0x02) ? Bundle.getMessage(POWER_STATE_ON) : Bundle.getMessage(POWER_STATE_OFF));
1091                        text.append("; ").append(Bundle.getMessage(X_NET_REPLY_CONTACT_LABEL)).append(" ").append(highnibble ? 7 : 3);
1092
1093                        text.append(" ").append(Bundle.getMessage(MAKE_LABEL, Bundle.getMessage(COLUMN_STATE))).append(" ")
1094                                .append(((getElement(i + 1) & 0x04) == 0x04) ? Bundle.getMessage(POWER_STATE_ON) : Bundle.getMessage(POWER_STATE_OFF));
1095                        text.append("; ").append(Bundle.getMessage(X_NET_REPLY_CONTACT_LABEL)).append(" ").append(highnibble ? 8 : 4);
1096
1097                        text.append(" ").append(Bundle.getMessage(MAKE_LABEL, Bundle.getMessage(COLUMN_STATE))).append(" ")
1098                                .append(((getElement(i + 1) & 0x08) == 0x08) ? Bundle.getMessage(POWER_STATE_ON) : Bundle.getMessage(POWER_STATE_OFF));
1099                        text.append("; ");
1100                        break;
1101                    default:
1102                        text.append(getElement(i)).append(" ").append(getElement(i + 1));
1103                }
1104            }
1105        } else {
1106            text = new StringBuilder(toString());
1107        }
1108        return text.toString();
1109    }
1110
1111    private void locoFunctionStatusHighText(StringBuilder text) {
1112        text.append(Bundle.getMessage(RS_TYPE)).append(" ");
1113        text.append(Bundle.getMessage("XNetReplyF13StatusLabel")).append(" ");
1114        // message byte 3, contains F20,F19,F18,F17,F16,F15,F14,F13
1115        int element3 = getElement(2);
1116        // message byte 4, contains F28,F27,F26,F25,F24,F23,F22,F21
1117        int element4 = getElement(3);
1118        text.append(parseFunctionHighStatus(element3, element4));
1119    }
1120
1121    private void locoFunctionStatusText(StringBuilder text) {
1122        text.append(Bundle.getMessage(RS_TYPE)).append(" "); // "Locomotive", key in NBBundle, shared with Operations
1123        text.append(Bundle.getMessage("XNetReplyFStatusLabel")).append(" ");
1124        // message byte 3, contains F0,F1,F2,F3,F4
1125        int element3 = getElement(2);
1126        // message byte 4, contains F12,F11,F10,F9,F8,F7,F6,F5
1127        int element4 = getElement(3);
1128        text.append(parseFunctionMomentaryStatus(element3, element4));
1129    }
1130
1131    private String getTurnoutReplyMonitorString(int startByte, String typeBundleKey) {
1132        StringBuilder text = new StringBuilder();
1133        int turnoutMsgAddr = getTurnoutMsgAddr(startByte);
1134        Optional<FeedbackItem> feedBackOdd = selectTurnoutFeedback(turnoutMsgAddr);
1135        if(feedBackOdd.isPresent()){
1136            FeedbackItem feedbackItem = feedBackOdd.get();
1137            text.append(singleTurnoutMonitorMessage(Bundle.getMessage(typeBundleKey), turnoutMsgAddr, feedbackItem));
1138            text.append(";");
1139            FeedbackItem pairedItem = feedbackItem.pairedAccessoryItem();
1140            text.append(singleTurnoutMonitorMessage("", turnoutMsgAddr + 1, pairedItem));
1141
1142        }
1143        return text.toString();
1144    }
1145
1146    private String singleTurnoutMonitorMessage(String prefix, int turnoutMsgAddr, FeedbackItem feedbackItem) {
1147        StringBuilder outputBuilder = new StringBuilder();
1148        outputBuilder.append(prefix).append(" ")
1149                .append(Bundle.getMessage(MAKE_LABEL, Bundle.getMessage(BEAN_NAME_TURNOUT))).append(" ")
1150                .append(turnoutMsgAddr).append(" ").append(Bundle.getMessage(MAKE_LABEL, Bundle.getMessage(COLUMN_STATE))).append(" ");
1151        switch (feedbackItem.getAccessoryStatus()){
1152            case 0:
1153                outputBuilder.append(Bundle.getMessage(X_NET_REPLY_NOT_OPERATED)); // last items on line, no trailing space
1154               break;
1155            case 1:
1156                outputBuilder.append(Bundle.getMessage(X_NET_REPLY_THROWN_LEFT));
1157               break;
1158            case 2:
1159                outputBuilder.append(Bundle.getMessage(X_NET_REPLY_THROWN_RIGHT));
1160                break;
1161            default:
1162                outputBuilder.append(Bundle.getMessage(X_NET_REPLY_INVALID));
1163        }
1164        if(feedbackItem.getType()==1){
1165            outputBuilder.append(" ");
1166            if(feedbackItem.isMotionComplete()){
1167                outputBuilder.append(Bundle.getMessage("XNetReplyMotionComplete"));
1168            } else {
1169                outputBuilder.append(Bundle.getMessage("XNetReplyMotionIncomplete"));
1170            }
1171        }
1172        return outputBuilder.toString();
1173    }
1174
1175    /**
1176     * Parse the speed step and the direction information for a locomotive.
1177     *
1178     * @param element1 contains the speed step mode designation and
1179     * availability information
1180     * @param element2 contains the data byte including the step mode and
1181     * availability information
1182     * @return readable version of message
1183     */
1184    protected String parseSpeedAndDirection(int element1, int element2) {
1185        String text = "";
1186        int speedVal;
1187        if ((element2 & 0x80) == 0x80) {
1188            text += Bundle.getMessage("Forward") + ",";
1189        } else {
1190            text += Bundle.getMessage("Reverse") + ",";
1191        }
1192
1193        if ((element1 & 0x04) == 0x04) {
1194            // We're in 128 speed step mode
1195            speedVal = element2 & 0x7f;
1196            // The first speed step used is actually at 2 for 128
1197            // speed step mode.
1198            if (speedVal >= 1) {
1199                speedVal -= 1;
1200            } else {
1201                speedVal = 0;
1202            }
1203            text += Bundle.getMessage(SPEED_STEP_MODE_X, 128) + ",";
1204        } else if ((element1 & 0x02) == 0x02) {
1205            // We're in 28 speed step mode
1206            // We have to re-arange the bits, since bit 4 is the LSB,
1207            // but other bits are in order from 0-3
1208            speedVal = ((element2 & 0x0F) << 1) + ((element2 & 0x10) >> 4);
1209            // The first speed step used is actually at 4 for 28  
1210            // speed step mode.
1211            if (speedVal >= 3) {
1212                speedVal -= 3;
1213            } else {
1214                speedVal = 0;
1215            }
1216            text += Bundle.getMessage(SPEED_STEP_MODE_X, 28) + ",";
1217        } else if ((element1 & 0x01) == 0x01) {
1218            // We're in 27 speed step mode
1219            // We have to re-arange the bits, since bit 4 is the LSB,
1220            // but other bits are in order from 0-3
1221            speedVal = ((element2 & 0x0F) << 1) + ((element2 & 0x10) >> 4);
1222            // The first speed step used is actually at 4 for 27
1223            // speed step mode.
1224            if (speedVal >= 3) {
1225                speedVal -= 3;
1226            } else {
1227                speedVal = 0;
1228            }
1229            text += Bundle.getMessage(SPEED_STEP_MODE_X, 27) + ",";
1230        } else {
1231            // Assume we're in 14 speed step mode.
1232            speedVal = (element2 & 0x0F);
1233            if (speedVal >= 1) {
1234                speedVal -= 1;
1235            } else {
1236                speedVal = 0;
1237            }
1238            text += Bundle.getMessage(SPEED_STEP_MODE_X, 14) + ",";
1239        }
1240
1241        text += Bundle.getMessage("SpeedStepLabel") + " " + speedVal + ". ";
1242
1243        if ((element1 & 0x08) == 0x08) {
1244            text += "" + Bundle.getMessage("XNetReplyAddressInUse");
1245        } else {
1246            text += "" + Bundle.getMessage("XNetReplyAddressFree");
1247        }
1248        return (text);
1249    }
1250
1251    /**
1252     * Parse the status of functions F0-F12.
1253     *
1254     * @param element3 contains the data byte including F0,F1,F2,F3,F4
1255     * @param element4 contains F12,F11,F10,F9,F8,F7,F6,F5
1256     * @return readable version of message
1257     */
1258    protected String parseFunctionStatus(int element3, int element4) {
1259        String text = "";
1260        if ((element3 & 0x10) != 0) {
1261            text += "F0 " + Bundle.getMessage(POWER_STATE_ON) + "; ";
1262        } else {
1263            text += "F0 " + Bundle.getMessage(POWER_STATE_OFF) + "; ";
1264        }
1265        if ((element3 & 0x01) != 0) {
1266            text += "F1 " + Bundle.getMessage(POWER_STATE_ON) + "; ";
1267        } else {
1268            text += "F1 " + Bundle.getMessage(POWER_STATE_OFF) + "; ";
1269        }
1270        if ((element3 & 0x02) != 0) {
1271            text += "F2 " + Bundle.getMessage(POWER_STATE_ON) + "; ";
1272        } else {
1273            text += "F2 " + Bundle.getMessage(POWER_STATE_OFF) + "; ";
1274        }
1275        if ((element3 & 0x04) != 0) {
1276            text += "F3 " + Bundle.getMessage(POWER_STATE_ON) + "; ";
1277        } else {
1278            text += "F3 " + Bundle.getMessage(POWER_STATE_OFF) + "; ";
1279        }
1280        if ((element3 & 0x08) != 0) {
1281            text += "F4 " + Bundle.getMessage(POWER_STATE_ON) + "; ";
1282        } else {
1283            text += "F4 " + Bundle.getMessage(POWER_STATE_OFF) + "; ";
1284        }
1285        if ((element4 & 0x01) != 0) {
1286            text += "F5 " + Bundle.getMessage(POWER_STATE_ON) + "; ";
1287        } else {
1288            text += "F5 " + Bundle.getMessage(POWER_STATE_OFF) + "; ";
1289        }
1290        if ((element4 & 0x02) != 0) {
1291            text += "F6 " + Bundle.getMessage(POWER_STATE_ON) + "; ";
1292        } else {
1293            text += "F6 " + Bundle.getMessage(POWER_STATE_OFF) + "; ";
1294        }
1295        if ((element4 & 0x04) != 0) {
1296            text += "F7 " + Bundle.getMessage(POWER_STATE_ON) + "; ";
1297        } else {
1298            text += "F7 " + Bundle.getMessage(POWER_STATE_OFF) + "; ";
1299        }
1300        if ((element4 & 0x08) != 0) {
1301            text += "F8 " + Bundle.getMessage(POWER_STATE_ON) + "; ";
1302        } else {
1303            text += "F8 " + Bundle.getMessage(POWER_STATE_OFF) + "; ";
1304        }
1305        if ((element4 & 0x10) != 0) {
1306            text += "F9 " + Bundle.getMessage(POWER_STATE_ON) + "; ";
1307        } else {
1308            text += "F9 " + Bundle.getMessage(POWER_STATE_OFF) + "; ";
1309        }
1310        if ((element4 & 0x20) != 0) {
1311            text += "F10 " + Bundle.getMessage(POWER_STATE_ON) + "; ";
1312        } else {
1313            text += "F10 " + Bundle.getMessage(POWER_STATE_OFF) + "; ";
1314        }
1315        if ((element4 & 0x40) != 0) {
1316            text += "F11 " + Bundle.getMessage(POWER_STATE_ON) + "; ";
1317        } else {
1318            text += "F11 " + Bundle.getMessage(POWER_STATE_OFF) + "; ";
1319        }
1320        if ((element4 & 0x80) != 0) {
1321            text += "F12 " + Bundle.getMessage(POWER_STATE_ON) + "; ";
1322        } else {
1323            text += "F12 " + Bundle.getMessage(POWER_STATE_OFF) + "; ";
1324        }
1325        return (text);
1326    }
1327
1328    /**
1329     * Parse the status of functions F13-F28.
1330     *
1331     * @param element3 contains F20,F19,F18,F17,F16,F15,F14,F13
1332     * @param element4 contains F28,F27,F26,F25,F24,F23,F22,F21
1333     * @return readable version of message
1334     */
1335    protected String parseFunctionHighStatus(int element3, int element4) {
1336        String text = "";
1337        if ((element3 & 0x01) != 0) {
1338            text += "F13 " + Bundle.getMessage(POWER_STATE_ON) + "; ";
1339        } else {
1340            text += "F13 " + Bundle.getMessage(POWER_STATE_OFF) + "; ";
1341        }
1342        if ((element3 & 0x02) != 0) {
1343            text += "F14 " + Bundle.getMessage(POWER_STATE_ON) + "; ";
1344        } else {
1345            text += "F14 " + Bundle.getMessage(POWER_STATE_OFF) + "; ";
1346        }
1347        if ((element3 & 0x04) != 0) {
1348            text += "F15 " + Bundle.getMessage(POWER_STATE_ON) + "; ";
1349        } else {
1350            text += "F15 " + Bundle.getMessage(POWER_STATE_OFF) + "; ";
1351        }
1352        if ((element3 & 0x08) != 0) {
1353            text += "F16 " + Bundle.getMessage(POWER_STATE_ON) + "; ";
1354        } else {
1355            text += "F16 " + Bundle.getMessage(POWER_STATE_OFF) + "; ";
1356        }
1357        if ((element3 & 0x10) != 0) {
1358            text += "F17 " + Bundle.getMessage(POWER_STATE_ON) + "; ";
1359        } else {
1360            text += "F17 " + Bundle.getMessage(POWER_STATE_OFF) + "; ";
1361        }
1362        if ((element3 & 0x20) != 0) {
1363            text += "F18 " + Bundle.getMessage(POWER_STATE_ON) + "; ";
1364        } else {
1365            text += "F18 " + Bundle.getMessage(POWER_STATE_OFF) + "; ";
1366        }
1367        if ((element3 & 0x40) != 0) {
1368            text += "F19 " + Bundle.getMessage(POWER_STATE_ON) + "; ";
1369        } else {
1370            text += "F19 " + Bundle.getMessage(POWER_STATE_OFF) + "; ";
1371        }
1372        if ((element3 & 0x80) != 0) {
1373            text += "F20 " + Bundle.getMessage(POWER_STATE_ON) + "; ";
1374        } else {
1375            text += "F20 " + Bundle.getMessage(POWER_STATE_OFF) + "; ";
1376        }
1377        if ((element4 & 0x01) != 0) {
1378            text += "F21 " + Bundle.getMessage(POWER_STATE_ON) + "; ";
1379        } else {
1380            text += "F21 " + Bundle.getMessage(POWER_STATE_OFF) + "; ";
1381        }
1382        if ((element4 & 0x02) != 0) {
1383            text += "F22 " + Bundle.getMessage(POWER_STATE_ON) + "; ";
1384        } else {
1385            text += "F22 " + Bundle.getMessage(POWER_STATE_OFF) + "; ";
1386        }
1387        if ((element4 & 0x04) != 0) {
1388            text += "F23 " + Bundle.getMessage(POWER_STATE_ON) + "; ";
1389        } else {
1390            text += "F23 " + Bundle.getMessage(POWER_STATE_OFF) + "; ";
1391        }
1392        if ((element4 & 0x08) != 0) {
1393            text += "F24 " + Bundle.getMessage(POWER_STATE_ON) + "; ";
1394        } else {
1395            text += "F24 " + Bundle.getMessage(POWER_STATE_OFF) + "; ";
1396        }
1397        if ((element4 & 0x10) != 0) {
1398            text += "F25 " + Bundle.getMessage(POWER_STATE_ON) + "; ";
1399        } else {
1400            text += "F25 " + Bundle.getMessage(POWER_STATE_OFF) + "; ";
1401        }
1402        if ((element4 & 0x20) != 0) {
1403            text += "F26 " + Bundle.getMessage(POWER_STATE_ON) + "; ";
1404        } else {
1405            text += "F26 " + Bundle.getMessage(POWER_STATE_OFF) + "; ";
1406        }
1407        if ((element4 & 0x40) != 0) {
1408            text += "F27 " + Bundle.getMessage(POWER_STATE_ON) + "; ";
1409        } else {
1410            text += "F27 " + Bundle.getMessage(POWER_STATE_OFF) + "; ";
1411        }
1412        if ((element4 & 0x80) != 0) {
1413            text += "F28 " + Bundle.getMessage(POWER_STATE_ON) + "; ";
1414        } else {
1415            text += "F28 " + Bundle.getMessage(POWER_STATE_OFF) + "; ";
1416        }
1417        return (text);
1418    }
1419    /**
1420     * Parse the Momentary status of functions.
1421     *
1422     * @param element3 contains the data byte including F0,F1,F2,F3,F4
1423     * @param element4 contains F12,F11,F10,F9,F8,F7,F6,F5
1424     * @return readable version of message
1425     */
1426    protected String parseFunctionMomentaryStatus(int element3, int element4) {
1427        String text = "";
1428        if ((element3 & 0x10) != 0) {
1429            text += "F0 " + Bundle.getMessage(FUNCTION_MOMENTARY) + "; ";
1430        } else {
1431            text += "F0 " + Bundle.getMessage(FUNCTION_CONTINUOUS) + "; ";
1432        }
1433        if ((element3 & 0x01) != 0) {
1434            text += "F1 " + Bundle.getMessage(FUNCTION_MOMENTARY) + "; ";
1435        } else {
1436            text += "F1 " + Bundle.getMessage(FUNCTION_CONTINUOUS) + "; ";
1437        }
1438        if ((element3 & 0x02) != 0) {
1439            text += "F2 " + Bundle.getMessage(FUNCTION_MOMENTARY) + "; ";
1440        } else {
1441            text += "F2 " + Bundle.getMessage(FUNCTION_CONTINUOUS) + "; ";
1442        }
1443        if ((element3 & 0x04) != 0) {
1444            text += "F3 " + Bundle.getMessage(FUNCTION_MOMENTARY) + "; ";
1445        } else {
1446            text += "F3 " + Bundle.getMessage(FUNCTION_CONTINUOUS) + "; ";
1447        }
1448        if ((element3 & 0x08) != 0) {
1449            text += "F4 " + Bundle.getMessage(FUNCTION_MOMENTARY) + "; ";
1450        } else {
1451            text += "F4 " + Bundle.getMessage(FUNCTION_CONTINUOUS) + "; ";
1452        }
1453        if ((element4 & 0x01) != 0) {
1454            text += "F5 " + Bundle.getMessage(FUNCTION_MOMENTARY) + "; ";
1455        } else {
1456            text += "F5 " + Bundle.getMessage(FUNCTION_CONTINUOUS) + "; ";
1457        }
1458        if ((element4 & 0x02) != 0) {
1459            text += "F6 " + Bundle.getMessage(FUNCTION_MOMENTARY) + "; ";
1460        } else {
1461            text += "F6 " + Bundle.getMessage(FUNCTION_CONTINUOUS) + "; ";
1462        }
1463        if ((element4 & 0x04) != 0) {
1464            text += "F7 " + Bundle.getMessage(FUNCTION_MOMENTARY) + "; ";
1465        } else {
1466            text += "F7 " + Bundle.getMessage(FUNCTION_CONTINUOUS) + "; ";
1467        }
1468        if ((element4 & 0x08) != 0) {
1469            text += "F8 " + Bundle.getMessage(FUNCTION_MOMENTARY) + "; ";
1470        } else {
1471            text += "F8 " + Bundle.getMessage(FUNCTION_CONTINUOUS) + "; ";
1472        }
1473        if ((element4 & 0x10) != 0) {
1474            text += "F9 " + Bundle.getMessage(FUNCTION_MOMENTARY) + "; ";
1475        } else {
1476            text += "F9 " + Bundle.getMessage(FUNCTION_CONTINUOUS) + "; ";
1477        }
1478        if ((element4 & 0x20) != 0) {
1479            text += "F10 " + Bundle.getMessage(FUNCTION_MOMENTARY) + "; ";
1480        } else {
1481            text += "F10 " + Bundle.getMessage(FUNCTION_CONTINUOUS) + "; ";
1482        }
1483        if ((element4 & 0x40) != 0) {
1484            text += "F11 " + Bundle.getMessage(FUNCTION_MOMENTARY) + "; ";
1485        } else {
1486            text += "F11 " + Bundle.getMessage(FUNCTION_CONTINUOUS) + "; ";
1487        }
1488        if ((element4 & 0x80) != 0) {
1489            text += "F12 " + Bundle.getMessage(FUNCTION_MOMENTARY) + "; ";
1490        } else {
1491            text += "F12 " + Bundle.getMessage(FUNCTION_CONTINUOUS) + "; ";
1492        }
1493        return (text);
1494    }
1495
1496    /**
1497     * Parse the Momentary sytatus of functions F13-F28.
1498     *
1499     * @param element3 contains F20,F19,F18,F17,F16,F15,F14,F13
1500     * @param element4 contains F28,F27,F26,F25,F24,F23,F22,F21
1501     * @return readable version of message
1502     */
1503    protected String parseFunctionHighMomentaryStatus(int element3, int element4) {
1504        String text = "";
1505        if ((element3 & 0x01) != 0) {
1506            text += "F13 " + Bundle.getMessage(FUNCTION_MOMENTARY) + "; ";
1507        } else {
1508            text += "F13 " + Bundle.getMessage(FUNCTION_CONTINUOUS) + "; ";
1509        }
1510        if ((element3 & 0x02) != 0) {
1511            text += "F14 " + Bundle.getMessage(FUNCTION_MOMENTARY) + "; ";
1512        } else {
1513            text += "F14 " + Bundle.getMessage(FUNCTION_CONTINUOUS) + "; ";
1514        }
1515        if ((element3 & 0x04) != 0) {
1516            text += "F15 " + Bundle.getMessage(FUNCTION_MOMENTARY) + "; ";
1517        } else {
1518            text += "F15 " + Bundle.getMessage(FUNCTION_CONTINUOUS) + "; ";
1519        }
1520        if ((element3 & 0x08) != 0) {
1521            text += "F16 " + Bundle.getMessage(FUNCTION_MOMENTARY) + "; ";
1522        } else {
1523            text += "F16 " + Bundle.getMessage(FUNCTION_CONTINUOUS) + "; ";
1524        }
1525        if ((element3 & 0x10) != 0) {
1526            text += "F17 " + Bundle.getMessage(FUNCTION_MOMENTARY) + "; ";
1527        } else {
1528            text += "F17 " + Bundle.getMessage(FUNCTION_CONTINUOUS) + "; ";
1529        }
1530        if ((element3 & 0x20) != 0) {
1531            text += "F18 " + Bundle.getMessage(FUNCTION_MOMENTARY) + "; ";
1532        } else {
1533            text += "F18 " + Bundle.getMessage(FUNCTION_CONTINUOUS) + "; ";
1534        }
1535        if ((element3 & 0x40) != 0) {
1536            text += "F19 " + Bundle.getMessage(FUNCTION_MOMENTARY) + "; ";
1537        } else {
1538            text += "F19 " + Bundle.getMessage(FUNCTION_CONTINUOUS) + "; ";
1539        }
1540        if ((element3 & 0x80) != 0) {
1541            text += "F20 " + Bundle.getMessage(FUNCTION_MOMENTARY) + "; ";
1542        } else {
1543            text += "F20 " + Bundle.getMessage(FUNCTION_CONTINUOUS) + "; ";
1544        }
1545        if ((element4 & 0x01) != 0) {
1546            text += "F21 " + Bundle.getMessage(FUNCTION_MOMENTARY) + "; ";
1547        } else {
1548            text += "F21 " + Bundle.getMessage(FUNCTION_CONTINUOUS) + "; ";
1549        }
1550        if ((element4 & 0x02) != 0) {
1551            text += "F22 " + Bundle.getMessage(FUNCTION_MOMENTARY) + "; ";
1552        } else {
1553            text += "F22 " + Bundle.getMessage(FUNCTION_CONTINUOUS) + "; ";
1554        }
1555        if ((element4 & 0x04) != 0) {
1556            text += "F23 " + Bundle.getMessage(FUNCTION_MOMENTARY) + "; ";
1557        } else {
1558            text += "F23 " + Bundle.getMessage(FUNCTION_CONTINUOUS) + "; ";
1559        }
1560        if ((element4 & 0x08) != 0) {
1561            text += "F24 " + Bundle.getMessage(FUNCTION_MOMENTARY) + "; ";
1562        } else {
1563            text += "F24 " + Bundle.getMessage(FUNCTION_CONTINUOUS) + "; ";
1564        }
1565        if ((element4 & 0x10) != 0) {
1566            text += "F25 " + Bundle.getMessage(FUNCTION_MOMENTARY) + "; ";
1567        } else {
1568            text += "F25 " + Bundle.getMessage(FUNCTION_CONTINUOUS) + "; ";
1569        }
1570        if ((element4 & 0x20) != 0) {
1571            text += "F26 " + Bundle.getMessage(FUNCTION_MOMENTARY) + "; ";
1572        } else {
1573            text += "F26 " + Bundle.getMessage(FUNCTION_CONTINUOUS) + "; ";
1574        }
1575        if ((element4 & 0x40) != 0) {
1576            text += "F27 " + Bundle.getMessage(FUNCTION_MOMENTARY) + "; ";
1577        } else {
1578            text += "F27 " + Bundle.getMessage(FUNCTION_CONTINUOUS) + "; ";
1579        }
1580        if ((element4 & 0x80) != 0) {
1581            text += "F28 " + Bundle.getMessage(FUNCTION_MOMENTARY) + "; ";
1582        } else {
1583            text += "F28 " + Bundle.getMessage(FUNCTION_CONTINUOUS) + "; ";
1584        }
1585        return (text);
1586    }
1587
1588}