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     * Mask to identify a turnout feedback + correct nibble. Turnout types differ in
625     * 6th bit, so it's left out (is variable).
626     */
627    private static final int FEEDBACK_TURNOUT_MASK = 0b0101_0000;
628
629    /**
630     * Mask to identify a feedback module + correct nibble. Turnout modules have
631     * type exactly 2.
632     */
633    private static final int FEEDBACK_MODULE_MASK  = 0b0111_0000;
634
635    /**
636     * The value of "feedback module" type.
637     */
638    private static final int FEEDBACK_TYPE_FBMODULE = 0b0100_0000;
639
640    /**
641     * Bit that indicates the higher nibble in module or turnout feedback
642     */
643    private static final int FEEDBACK_HIGH_NIBBLE = 0b0001_0000;
644
645    private int findFeedbackData(int baseAddress, int selector, int mask) {
646        if (isFeedbackMessage()) {
647            // shorctcut for single-item msg
648            int data = getElement(2);
649            if (getElement(1) == baseAddress &&
650                (data & mask) == selector) {
651                return data;
652            }
653        } else {
654            int start = 1;
655            for (int cnt = getFeedbackMessageItems(); cnt > 0; cnt--, start += 2) {
656                int data = getElement(start + 1);
657                if (getElement(start) == baseAddress &&
658                    (data & mask) == selector) {
659                    return data;
660                }
661            }
662        }
663        return -1;
664    }
665
666    /**
667     * Returns value of the given feedback module bit. Returns {@link Optional}
668     * that is non-empty, if the feedback was present. The Optional's value indicates the
669     * feedback state.
670     *
671     * @param sensorNumber the sensor bit ID
672     * @return optional sensor state.
673     */
674    @CheckForNull
675    public Boolean selectModuleFeedback(int sensorNumber) {
676        if (!isFeedbackBroadcastMessage() || sensorNumber == 0 || sensorNumber >= 1024) {
677            return null;
678        }
679        // feedback address directly addresses 8-bit module, XpressNet spec 3.0:2.1.11.
680        int s = sensorNumber - 1;
681        int baseAddress = (s / 8);
682        int selector2 = (s & 0x04) != 0 ?
683                FEEDBACK_TYPE_FBMODULE | FEEDBACK_HIGH_NIBBLE :
684                FEEDBACK_TYPE_FBMODULE;
685        int res = findFeedbackData(baseAddress, selector2, FEEDBACK_MODULE_MASK);
686        return res == -1 ? null :
687                (res & (1 << (s % 4))) > 0;
688    }
689
690    /**
691     * Calls processor for turnout's feedback, returns the processor's outcome.
692     * Searches for the turnout feedback for the given accessory. If found,
693     * runs a processor on the feedback item, and returns its Boolean result.
694     * <p>
695     * Returns {@code false}, if matching feedback is not found.
696     * @param accessoryNumber the turnout number
697     * @param proc the processor
698     * @return {@code false} if feedback was not found, or a result of {@code proc()}.
699     */
700    public boolean onTurnoutFeedback(int accessoryNumber, Function<FeedbackItem, Boolean> proc) {
701        return selectTurnoutFeedback(accessoryNumber).map(proc).orElse(false);
702    }
703
704    /**
705     * Selects a matching turnout feedback. Finds turnout feedback for the given {@code accessoryNumber}.
706     * Returns an encapsulated feedback, that can be inspected. If no matching feedback is
707     * present, returns empty {@link Optional}.
708     * @param accessoryNumber the turnout number
709     * @return optional feedback item.
710     */
711    @Nonnull
712    public Optional<FeedbackItem> selectTurnoutFeedback(int accessoryNumber) {
713        // shortcut for single-item messages.
714        if (!isFeedbackBroadcastMessage() || accessoryNumber <= 0 || accessoryNumber >= 1024) {
715            return Optional.empty();
716        }
717        int a = accessoryNumber - 1;
718        int base = (a / 4);
719        // the mask makes the turnout feedback type bit irrelevant
720        int selector2 = (a & 0x02) != 0 ? FEEDBACK_HIGH_NIBBLE : 0;
721        int r = findFeedbackData(base, selector2, FEEDBACK_TURNOUT_MASK);
722        if (r == -1) {
723            return Optional.empty();
724        }
725        FeedbackItem item = new FeedbackItem(this, accessoryNumber, r);
726        return Optional.of(item);
727    }
728
729    protected final FeedbackItem createFeedbackItem(int n, int d) {
730        return new FeedbackItem(this, n, d);
731    }
732
733    /**
734     * @return a string representation of the reply suitable for display in the
735     * XpressNet monitor.
736     */
737    @Override
738    public String toMonitorString(){
739        StringBuilder text;
740        // First, Decode anything that is sent by the LI10x, and
741        // not the command station
742
743        if(getElement(0) == XNetConstants.LI_MESSAGE_RESPONSE_HEADER){
744            switch(this.getElement(1)) {
745              case XNetConstants.LI_MESSAGE_RESPONSE_PC_DATA_ERROR:
746                 text = new StringBuilder(Bundle.getMessage("XNetReplyErrorPCtoLI"));
747                 break;
748              case XNetConstants.LI_MESSAGE_RESPONSE_CS_DATA_ERROR:
749                 text = new StringBuilder(Bundle.getMessage("XNetReplyErrorLItoCS"));
750                 break;
751              case XNetConstants.LI_MESSAGE_RESPONSE_UNKNOWN_DATA_ERROR:
752                 text = new StringBuilder(Bundle.getMessage("XNetReplyErrorUnknown"));
753                 break;
754              case XNetConstants.LI_MESSAGE_RESPONSE_SEND_SUCCESS:
755                 text = new StringBuilder(Bundle.getMessage("XNetReplyOkMessage"));
756                 break;
757              case XNetConstants.LI_MESSAGE_RESPONSE_TIMESLOT_ERROR:
758                 text = new StringBuilder(Bundle.getMessage("XNetReplyErrorNoTimeSlot"));
759                 break;
760              case XNetConstants.LI_MESSAGE_RESPONSE_BUFFER_OVERFLOW:
761                 text = new StringBuilder(Bundle.getMessage("XNetReplyErrorBufferOverflow"));
762                 break;
763              case XNetConstants.LIUSB_TIMESLOT_RESTORED:
764                 text = new StringBuilder(Bundle.getMessage("XNetReplyTimeSlotRestored"));
765                 break;
766              case XNetConstants.LIUSB_REQUEST_SENT_WHILE_NO_TIMESLOT:
767                 text = new StringBuilder(Bundle.getMessage("XNetReplyRequestSentWhileNoTimeslot"));
768                 break;
769              case XNetConstants.LIUSB_BAD_DATA_IN_REQUEST:
770                 text = new StringBuilder(Bundle.getMessage("XNetReplyBadDataInRequest"));
771                 break;
772              case XNetConstants.LIUSB_RETRANSMIT_REQUEST:
773                 text = new StringBuilder(Bundle.getMessage("XNetReplyRetransmitRequest"));
774                 break;
775              default:
776                 text = new StringBuilder(toString());
777           }
778        } else if (getElement(0) == XNetConstants.LI_VERSION_RESPONSE) {
779            text = new StringBuilder(Bundle.getMessage("XNetReplyLIVersion", (getElementBCD(1).floatValue()) / 10, (getElementBCD(2).floatValue()) / 10));
780        } else if (getElement(0) == XNetConstants.LI101_REQUEST) {
781            // The request and response for baud rate look the same,
782            // so we need this for both incoming and outgoing directions
783            switch (getElement(1)) {
784                case XNetConstants.LI101_REQUEST_ADDRESS:
785                    text = new StringBuilder(Bundle.getMessage("XNetReplyLIAddress", getElement(2)));
786                    break;
787                case XNetConstants.LI101_REQUEST_BAUD:
788                    switch (getElement(2)) {
789                        case 1:
790                            text = new StringBuilder(Bundle.getMessage(X_NET_REPLY_LI_BAUD, Bundle.getMessage("LIBaud19200")));
791                            break;
792                        case 2:
793                            text = new StringBuilder(Bundle.getMessage(X_NET_REPLY_LI_BAUD, Bundle.getMessage("Baud38400")));
794                            break;
795                        case 3:
796                            text = new StringBuilder(Bundle.getMessage(X_NET_REPLY_LI_BAUD, Bundle.getMessage("Baud57600")));
797                            break;
798                        case 4:
799                            text = new StringBuilder(Bundle.getMessage(X_NET_REPLY_LI_BAUD, Bundle.getMessage("Baud115200")));
800                            break;
801                        default:
802                            text = new StringBuilder(Bundle.getMessage(X_NET_REPLY_LI_BAUD, Bundle.getMessage("BaudOther")));
803                    }
804                    break;
805                default:
806                    text = new StringBuilder(toString());
807            }
808            /* Next, check the "CS Info" messages */
809        } else if (getElement(0) == XNetConstants.CS_INFO) {
810            switch (getElement(1)) {
811                case XNetConstants.BC_NORMAL_OPERATIONS:
812                    text = new StringBuilder(Bundle.getMessage("XNetReplyBCNormalOpsResumed"));
813                    break;
814                case XNetConstants.BC_EVERYTHING_OFF:
815                    text = new StringBuilder(Bundle.getMessage("XNetReplyBCEverythingOff"));
816                    break;
817                case XNetConstants.BC_SERVICE_MODE_ENTRY:
818                    text = new StringBuilder(Bundle.getMessage("XNetReplyBCServiceEntry"));
819                    break;
820                case XNetConstants.PROG_SHORT_CIRCUIT:
821                    text = new StringBuilder(Bundle.getMessage("XNetReplyServiceModeShort"));
822                    break;
823                case XNetConstants.PROG_BYTE_NOT_FOUND:
824                    text = new StringBuilder(Bundle.getMessage("XNetReplyServiceModeDataByteNotFound"));
825                    break;
826                case XNetConstants.PROG_CS_BUSY:
827                    text = new StringBuilder(Bundle.getMessage("XNetReplyServiceModeCSBusy"));
828                    break;
829                case XNetConstants.PROG_CS_READY:
830                    text = new StringBuilder(Bundle.getMessage("XNetReplyServiceModeCSReady"));
831                    break;
832                case XNetConstants.CS_BUSY:
833                    text = new StringBuilder(Bundle.getMessage("XNetReplyCSBusy"));
834                    break;
835                case XNetConstants.CS_NOT_SUPPORTED:
836                    text = new StringBuilder(Bundle.getMessage("XNetReplyCSNotSupported"));
837                    break;
838                case XNetConstants.CS_TRANSFER_ERROR:
839                    text = new StringBuilder(Bundle.getMessage("XNetReplyCSTransferError"));
840                    break;
841                /* The remaining cases are for a Double Header or MU Error */
842                case XNetConstants.CS_DH_ERROR_NON_OP:
843                    text = new StringBuilder(Bundle.getMessage("XNetReplyV1DHErrorNotOperated"));
844                    break;
845                case XNetConstants.CS_DH_ERROR_IN_USE:
846                    text = new StringBuilder(Bundle.getMessage("XNetReplyV1DHErrorInUse"));
847                    break;
848                case XNetConstants.CS_DH_ERROR_ALREADY_DH:
849                    text = new StringBuilder(Bundle.getMessage("XNetReplyV1DHErrorAlreadyDH"));
850                    break;
851                case XNetConstants.CS_DH_ERROR_NONZERO_SPD:
852                    text = new StringBuilder(Bundle.getMessage("XNetReplyV1DHErrorNonZeroSpeed"));
853                    break;
854                default:
855                    text = new StringBuilder(toString());
856            }
857        } else if (getElement(0) == XNetConstants.BC_EMERGENCY_STOP
858                && getElement(1) == XNetConstants.BC_EVERYTHING_STOP) {
859            text = new StringBuilder(Bundle.getMessage("XNetReplyBCEverythingStop"));
860            /* Followed by Service Mode responses */
861        } else if (getElement(0) == XNetConstants.CS_SERVICE_MODE_RESPONSE) {
862            if (isDirectModeResponse()) {
863                text = new StringBuilder(Bundle.getMessage("XNetReplyServiceModeDirectResponse", getServiceModeCVNumber(), getServiceModeCVValue()));
864            } else if (isPagedModeResponse()) {
865                text = new StringBuilder(Bundle.getMessage("XNetReplyServiceModePagedResponse", getServiceModeCVNumber(), getServiceModeCVValue()));
866            } else if (getElement(1) == XNetConstants.CS_SOFTWARE_VERSION) {
867                String typeString;
868                switch (getElement(3)) {
869                    case 0x00:
870                        typeString = Bundle.getMessage("CSTypeLZ100");
871                        break;
872                    case 0x01:
873                        typeString = Bundle.getMessage("CSTypeLH200");
874                        break;
875                    case 0x02:
876                        typeString = Bundle.getMessage("CSTypeCompact");
877                        break;
878                    // GT 2007/11/6 - Added multiMaus
879                    case 0x10:
880                        typeString = Bundle.getMessage("CSTypeMultiMaus");
881                        break;
882                    default:
883                        typeString = "" + getElement(3);
884                }
885                text = new StringBuilder(Bundle.getMessage("XNetReplyCSVersion", (getElementBCD(2).floatValue()) / 10, typeString));
886            } else {
887                text = new StringBuilder(toString());
888            }
889 /* We want to look at responses to specific requests made to the Command Station */
890        } else if (getElement(0) == XNetConstants.CS_REQUEST_RESPONSE) {
891            if (getElement(1) == XNetConstants.CS_STATUS_RESPONSE) {
892                text = new StringBuilder(Bundle.getMessage("XNetReplyCSStatus") + " ");
893                int statusByte = getElement(2);
894                if ((statusByte & 0x01) == 0x01) {
895                    // Command station is in Emergency Off Mode
896                    text.append(Bundle.getMessage("XNetCSStatusEmergencyOff")).append("; ");
897                }
898                if ((statusByte & 0x02) == 0x02) {
899                    // Command station is in Emergency Stop Mode
900                    text.append(Bundle.getMessage("XNetCSStatusEmergencyStop")).append("; ");
901                }
902                if ((statusByte & 0x08) == 0x08) {
903                    // Command station is in Service Mode
904                    text.append(Bundle.getMessage("XNetCSStatusServiceMode")).append("; ");
905                }
906                if ((statusByte & 0x40) == 0x40) {
907                    // Command station is in Power Up Mode
908                    text.append(Bundle.getMessage("XNetCSStatusPoweringUp")).append("; ");
909                }
910                if ((statusByte & 0x04) == 0x04) {
911                    text.append(Bundle.getMessage("XNetCSStatusPowerModeAuto")).append("; ");
912                } else {
913                    text.append(Bundle.getMessage("XNetCSStatusPowerModeManual")).append("; ");
914                }
915                if ((statusByte & 0x80) == 0x80) {
916                    // Command station has a experienced a ram check error
917                    text.append(Bundle.getMessage("XNetCSStatusRamCheck"));
918                }
919            } else if (getElement(1) == XNetConstants.CS_SOFTWARE_VERSION) {
920                /* This is a Software version response for XpressNet
921                 Version 1 or 2 */
922                text = new StringBuilder(Bundle.getMessage("XNetReplyCSVersionV1", (getElementBCD(2).floatValue()) / 10));
923            } else {
924                text = new StringBuilder(toString());
925            }
926
927            // MU and Double Header Related Responses
928        } else if (getElement(0) == XNetConstants.LOCO_MU_DH_ERROR) {
929            switch (getElement(1)) {
930                case 0x81:
931                    text = new StringBuilder(Bundle.getMessage("XNetReplyDHErrorNotOperated"));
932                    break;
933                case 0x82:
934                    text = new StringBuilder(Bundle.getMessage("XNetReplyDHErrorInUse"));
935                    break;
936                case 0x83:
937                    text = new StringBuilder(Bundle.getMessage("XNetReplyDHErrorAlreadyDH"));
938                    break;
939                case 0x84:
940                    text = new StringBuilder(Bundle.getMessage("XNetReplyDHErrorNonZeroSpeed"));
941                    break;
942                case 0x85:
943                    text = new StringBuilder(Bundle.getMessage("XNetReplyDHErrorLocoNotMU"));
944                    break;
945                case 0x86:
946                    text = new StringBuilder(Bundle.getMessage("XNetReplyDHErrorLocoNotMUBase"));
947                    break;
948                case 0x87:
949                    text = new StringBuilder(Bundle.getMessage("XNetReplyDHErrorCanNotDelete"));
950                    break;
951                case 0x88:
952                    text = new StringBuilder(Bundle.getMessage("XNetReplyDHErrorStackFull"));
953                    break;
954                default:
955                    text = new StringBuilder(Bundle.getMessage("XNetReplyDHErrorOther", (getElement(1) - 0x80)));
956            }
957            // Loco Information Response Messages
958        } else if (getElement(0) == XNetConstants.LOCO_INFO_NORMAL_UNIT) {
959            if (getElement(1) == XNetConstants.LOCO_FUNCTION_STATUS_HIGH_MOM) {
960                text = new StringBuilder(Bundle.getMessage("XNetReplyLocoStatus13Label") + " ");
961                // message byte 3, contains F20,F19,F18,F17,F16,F15,F14,F13
962                int element3 = getElement(2);
963                // message byte 4, contains F28,F27,F26,F25,F24,F23,F22,F21
964                int element4 = getElement(3);
965                text.append(parseFunctionHighMomentaryStatus(element3, element4));
966            } else {
967                text = new StringBuilder(Bundle.getMessage("XNetReplyLocoNormalLabel") + ",");
968                text.append(parseSpeedAndDirection(getElement(1), getElement(2))).append(" ");
969                // message byte 4, contains F0,F1,F2,F3,F4
970                int element3 = getElement(3);
971                // message byte 5, contains F12,F11,F10,F9,F8,F7,F6,F5
972                int element4 = getElement(4);
973                text.append(parseFunctionStatus(element3, element4));
974            }
975        } else if (getElement(0) == XNetConstants.LOCO_INFO_MUED_UNIT) {
976            if (getElement(1) == 0xF8) {
977                // This message is a Hornby addition to the protocol
978                // indicating the speed and direction of a locomoitve
979                // controlled by the elite's built in throttles
980                text = new StringBuilder(Bundle.getMessage("XNetReplyLocoEliteSLabel") + " ");
981                text.append(LenzCommandStation.calcLocoAddress(getElement(2), getElement(3)));
982                text.append(",").append(parseSpeedAndDirection(getElement(4), getElement(5))).append(" ");
983            } else if (getElement(1) == 0xF9) {
984                // This message is a Hornby addition to the protocol
985                // indicating the function on/off status of a locomoitve
986                // controlled by the elite's built in throttles
987                text = new StringBuilder(Bundle.getMessage("XNetReplyLocoEliteFLabel") + " ");
988                text.append(LenzCommandStation.calcLocoAddress(getElement(2), getElement(3))).append(" ");
989                // message byte 5, contains F0,F1,F2,F3,F4
990                int element4 = getElement(4);
991                // message byte 5, contains F12,F11,F10,F9,F8,F7,F6,F5
992                int element5 = getElement(5);
993                text.append(parseFunctionStatus(element4, element5));
994            } else {
995                text = new StringBuilder(Bundle.getMessage("XNetReplyLocoMULabel") + ",");
996                text.append(parseSpeedAndDirection(getElement(1), getElement(2)));
997                // message byte 4, contains F0,F1,F2,F3,F4
998                int element3 = getElement(3);
999                // message byte 5, contains F12,F11,F10,F9,F8,F7,F6,F5
1000                int element4 = getElement(4);
1001                text.append(parseFunctionStatus(element3, element4));
1002            }
1003        } else if (getElement(0) == XNetConstants.LOCO_INFO_MU_ADDRESS) {
1004            text = new StringBuilder(Bundle.getMessage("XNetReplyLocoMUBaseLabel") + ",");
1005            text.append(parseSpeedAndDirection(getElement(1), getElement(2))).append(" ");
1006        } else if (getElement(0) == XNetConstants.LOCO_INFO_DH_UNIT) {
1007            text = new StringBuilder(Bundle.getMessage("XNetReplyLocoDHLabel") + ",");
1008            text.append(parseSpeedAndDirection(getElement(1), getElement(2))).append(" ");
1009            // message byte 4, contains F0,F1,F2,F3,F4
1010            int element3 = getElement(3);
1011            // message byte 5, contains F12,F11,F10,F9,F8,F7,F6,F5
1012            int element4 = getElement(4);
1013            text.append(parseFunctionStatus(element3, element4));
1014            text.append(" ").append(Bundle.getMessage("XNetReplyLoco2DHLabel")).append(" ");
1015            text.append(LenzCommandStation.calcLocoAddress(getElement(5), getElement(6)));
1016        } else if (getElement(0) == XNetConstants.LOCO_INFO_RESPONSE) {
1017            text = new StringBuilder(Bundle.getMessage("XNetReplyLocoLabel") + " ");
1018            switch (getElement(1)) {
1019                case XNetConstants.LOCO_SEARCH_RESPONSE_N:
1020                    text.append(Bundle.getMessage("XNetReplySearchNormalLabel")).append(" ");
1021                    text.append(getThrottleMsgAddr());
1022                    break;
1023                case XNetConstants.LOCO_SEARCH_RESPONSE_DH:
1024                    text.append(Bundle.getMessage("XNetReplySearchDHLabel")).append(" ");
1025                    text.append(getThrottleMsgAddr());
1026                    break;
1027                case XNetConstants.LOCO_SEARCH_RESPONSE_MU_BASE:
1028                    text.append(Bundle.getMessage("XNetReplySearchMUBaseLabel")).append(" ");
1029                    text.append(getThrottleMsgAddr());
1030                    break;
1031                case XNetConstants.LOCO_SEARCH_RESPONSE_MU:
1032                    text.append(Bundle.getMessage("XNetReplySearchMULabel")).append(" ");
1033                    text.append(getThrottleMsgAddr());
1034                    break;
1035                case XNetConstants.LOCO_SEARCH_NO_RESULT:
1036                    text.append(Bundle.getMessage("XNetReplySearchFailedLabel")).append(" ");
1037                    text.append(getThrottleMsgAddr());
1038                    break;
1039                case XNetConstants.LOCO_NOT_AVAILABLE:
1040                    text.append(Bundle.getMessage(RS_TYPE)).append(" ");
1041                    text.append(getThrottleMsgAddr()).append(" ");
1042                    text.append(Bundle.getMessage("XNetReplyLocoOperated"));
1043                    break;
1044                case XNetConstants.LOCO_FUNCTION_STATUS:
1045                    locoFunctionStatusText(text);
1046                    break;
1047                case XNetConstants.LOCO_FUNCTION_STATUS_HIGH:
1048                    locoFunctionStatusHighText(text);
1049                    break;
1050                default:
1051                    text = new StringBuilder(toString());
1052            }
1053            // Feedback Response Messages
1054        } else if (isFeedbackBroadcastMessage()) {
1055            text = new StringBuilder().append(Bundle.getMessage("XNetReplyFeedbackLabel")).append(" ");
1056            int numDataBytes = getElement(0) & 0x0f;
1057            for (int i = 1; i < numDataBytes; i += 2) {
1058                switch (getFeedbackMessageType(i)) {
1059                    case 0:
1060                        text.append(getTurnoutReplyMonitorString(i, "TurnoutWoFeedback"));
1061                        break;
1062                    case 1:
1063                        text.append(getTurnoutReplyMonitorString(i, "TurnoutWFeedback"));
1064                        break;
1065                    case 2:
1066                        text.append(Bundle.getMessage("XNetReplyFeedbackEncoder")).append(" ").append(getFeedbackEncoderMsgAddr(i) + 1);
1067                        boolean highnibble = ((getElement(i + 1) & 0x10) == 0x10);
1068                        text.append(" ").append(Bundle.getMessage(X_NET_REPLY_CONTACT_LABEL)).append(" ").append(highnibble ? 5 : 1);
1069
1070                        text.append(" ").append(Bundle.getMessage(MAKE_LABEL, Bundle.getMessage(COLUMN_STATE))).append(" ")
1071                                .append(((getElement(i + 1) & 0x01) == 0x01) ? Bundle.getMessage(POWER_STATE_ON) : Bundle.getMessage(POWER_STATE_OFF));
1072                        text.append("; ").append(Bundle.getMessage(X_NET_REPLY_CONTACT_LABEL)).append(" ").append(highnibble ? 6 : 2);
1073
1074                        text.append(" ").append(Bundle.getMessage(MAKE_LABEL, Bundle.getMessage(COLUMN_STATE))).append(" ")
1075                                .append(((getElement(i + 1) & 0x02) == 0x02) ? Bundle.getMessage(POWER_STATE_ON) : Bundle.getMessage(POWER_STATE_OFF));
1076                        text.append("; ").append(Bundle.getMessage(X_NET_REPLY_CONTACT_LABEL)).append(" ").append(highnibble ? 7 : 3);
1077
1078                        text.append(" ").append(Bundle.getMessage(MAKE_LABEL, Bundle.getMessage(COLUMN_STATE))).append(" ")
1079                                .append(((getElement(i + 1) & 0x04) == 0x04) ? Bundle.getMessage(POWER_STATE_ON) : Bundle.getMessage(POWER_STATE_OFF));
1080                        text.append("; ").append(Bundle.getMessage(X_NET_REPLY_CONTACT_LABEL)).append(" ").append(highnibble ? 8 : 4);
1081
1082                        text.append(" ").append(Bundle.getMessage(MAKE_LABEL, Bundle.getMessage(COLUMN_STATE))).append(" ")
1083                                .append(((getElement(i + 1) & 0x08) == 0x08) ? Bundle.getMessage(POWER_STATE_ON) : Bundle.getMessage(POWER_STATE_OFF));
1084                        text.append("; ");
1085                        break;
1086                    default:
1087                        text.append(getElement(i)).append(" ").append(getElement(i + 1));
1088                }
1089            }
1090        } else {
1091            text = new StringBuilder(toString());
1092        }
1093        return text.toString();
1094    }
1095
1096    private void locoFunctionStatusHighText(StringBuilder text) {
1097        text.append(Bundle.getMessage(RS_TYPE)).append(" ");
1098        text.append(Bundle.getMessage("XNetReplyF13StatusLabel")).append(" ");
1099        // message byte 3, contains F20,F19,F18,F17,F16,F15,F14,F13
1100        int element3 = getElement(2);
1101        // message byte 4, contains F28,F27,F26,F25,F24,F23,F22,F21
1102        int element4 = getElement(3);
1103        text.append(parseFunctionHighStatus(element3, element4));
1104    }
1105
1106    private void locoFunctionStatusText(StringBuilder text) {
1107        text.append(Bundle.getMessage(RS_TYPE)).append(" "); // "Locomotive", key in NBBundle, shared with Operations
1108        text.append(Bundle.getMessage("XNetReplyFStatusLabel")).append(" ");
1109        // message byte 3, contains F0,F1,F2,F3,F4
1110        int element3 = getElement(2);
1111        // message byte 4, contains F12,F11,F10,F9,F8,F7,F6,F5
1112        int element4 = getElement(3);
1113        text.append(parseFunctionMomentaryStatus(element3, element4));
1114    }
1115
1116    private String getTurnoutReplyMonitorString(int startByte, String typeBundleKey) {
1117        StringBuilder text = new StringBuilder();
1118        int turnoutMsgAddr = getTurnoutMsgAddr(startByte);
1119        Optional<FeedbackItem> feedBackOdd = selectTurnoutFeedback(turnoutMsgAddr);
1120        if(feedBackOdd.isPresent()){
1121            FeedbackItem feedbackItem = feedBackOdd.get();
1122            text.append(singleTurnoutMonitorMessage(Bundle.getMessage(typeBundleKey), turnoutMsgAddr, feedbackItem));
1123            text.append(";");
1124            FeedbackItem pairedItem = feedbackItem.pairedAccessoryItem();
1125            text.append(singleTurnoutMonitorMessage("", turnoutMsgAddr + 1, pairedItem));
1126
1127        }
1128        return text.toString();
1129    }
1130
1131    private String singleTurnoutMonitorMessage(String prefix, int turnoutMsgAddr, FeedbackItem feedbackItem) {
1132        StringBuilder outputBuilder = new StringBuilder();
1133        outputBuilder.append(prefix).append(" ")
1134                .append(Bundle.getMessage(MAKE_LABEL, Bundle.getMessage(BEAN_NAME_TURNOUT))).append(" ")
1135                .append(turnoutMsgAddr).append(" ").append(Bundle.getMessage(MAKE_LABEL, Bundle.getMessage(COLUMN_STATE))).append(" ");
1136        switch (feedbackItem.getAccessoryStatus()){
1137            case 0:
1138                outputBuilder.append(Bundle.getMessage(X_NET_REPLY_NOT_OPERATED)); // last items on line, no trailing space
1139               break;
1140            case 1:
1141                outputBuilder.append(Bundle.getMessage(X_NET_REPLY_THROWN_LEFT));
1142               break;
1143            case 2:
1144                outputBuilder.append(Bundle.getMessage(X_NET_REPLY_THROWN_RIGHT));
1145                break;
1146            default:
1147                outputBuilder.append(Bundle.getMessage(X_NET_REPLY_INVALID));
1148        }
1149        if(feedbackItem.getType()==1){
1150            outputBuilder.append(" ");
1151            if(feedbackItem.isMotionComplete()){
1152                outputBuilder.append(Bundle.getMessage("XNetReplyMotionComplete"));
1153            } else {
1154                outputBuilder.append(Bundle.getMessage("XNetReplyMotionIncomplete"));
1155            }
1156        }
1157        return outputBuilder.toString();
1158    }
1159
1160    /**
1161     * Parse the speed step and the direction information for a locomotive.
1162     *
1163     * @param element1 contains the speed step mode designation and
1164     * availability information
1165     * @param element2 contains the data byte including the step mode and
1166     * availability information
1167     * @return readable version of message
1168     */
1169    protected String parseSpeedAndDirection(int element1, int element2) {
1170        String text = "";
1171        int speedVal;
1172        if ((element2 & 0x80) == 0x80) {
1173            text += Bundle.getMessage("Forward") + ",";
1174        } else {
1175            text += Bundle.getMessage("Reverse") + ",";
1176        }
1177
1178        if ((element1 & 0x04) == 0x04) {
1179            // We're in 128 speed step mode
1180            speedVal = element2 & 0x7f;
1181            // The first speed step used is actually at 2 for 128
1182            // speed step mode.
1183            if (speedVal >= 1) {
1184                speedVal -= 1;
1185            } else {
1186                speedVal = 0;
1187            }
1188            text += Bundle.getMessage(SPEED_STEP_MODE_X, 128) + ",";
1189        } else if ((element1 & 0x02) == 0x02) {
1190            // We're in 28 speed step mode
1191            // We have to re-arange the bits, since bit 4 is the LSB,
1192            // but other bits are in order from 0-3
1193            speedVal = ((element2 & 0x0F) << 1) + ((element2 & 0x10) >> 4);
1194            // The first speed step used is actually at 4 for 28
1195            // speed step mode.
1196            if (speedVal >= 3) {
1197                speedVal -= 3;
1198            } else {
1199                speedVal = 0;
1200            }
1201            text += Bundle.getMessage(SPEED_STEP_MODE_X, 28) + ",";
1202        } else if ((element1 & 0x01) == 0x01) {
1203            // We're in 27 speed step mode
1204            // We have to re-arange the bits, since bit 4 is the LSB,
1205            // but other bits are in order from 0-3
1206            speedVal = ((element2 & 0x0F) << 1) + ((element2 & 0x10) >> 4);
1207            // The first speed step used is actually at 4 for 27
1208            // speed step mode.
1209            if (speedVal >= 3) {
1210                speedVal -= 3;
1211            } else {
1212                speedVal = 0;
1213            }
1214            text += Bundle.getMessage(SPEED_STEP_MODE_X, 27) + ",";
1215        } else {
1216            // Assume we're in 14 speed step mode.
1217            speedVal = (element2 & 0x0F);
1218            if (speedVal >= 1) {
1219                speedVal -= 1;
1220            } else {
1221                speedVal = 0;
1222            }
1223            text += Bundle.getMessage(SPEED_STEP_MODE_X, 14) + ",";
1224        }
1225
1226        text += Bundle.getMessage("SpeedStepLabel") + " " + speedVal + ". ";
1227
1228        if ((element1 & 0x08) == 0x08) {
1229            text += "" + Bundle.getMessage("XNetReplyAddressInUse");
1230        } else {
1231            text += "" + Bundle.getMessage("XNetReplyAddressFree");
1232        }
1233        return (text);
1234    }
1235
1236    /**
1237     * Parse the status of functions F0-F12.
1238     *
1239     * @param element3 contains the data byte including F0,F1,F2,F3,F4
1240     * @param element4 contains F12,F11,F10,F9,F8,F7,F6,F5
1241     * @return readable version of message
1242     */
1243    protected String parseFunctionStatus(int element3, int element4) {
1244        String text = "";
1245        if ((element3 & 0x10) != 0) {
1246            text += "F0 " + Bundle.getMessage(POWER_STATE_ON) + "; ";
1247        } else {
1248            text += "F0 " + Bundle.getMessage(POWER_STATE_OFF) + "; ";
1249        }
1250        if ((element3 & 0x01) != 0) {
1251            text += "F1 " + Bundle.getMessage(POWER_STATE_ON) + "; ";
1252        } else {
1253            text += "F1 " + Bundle.getMessage(POWER_STATE_OFF) + "; ";
1254        }
1255        if ((element3 & 0x02) != 0) {
1256            text += "F2 " + Bundle.getMessage(POWER_STATE_ON) + "; ";
1257        } else {
1258            text += "F2 " + Bundle.getMessage(POWER_STATE_OFF) + "; ";
1259        }
1260        if ((element3 & 0x04) != 0) {
1261            text += "F3 " + Bundle.getMessage(POWER_STATE_ON) + "; ";
1262        } else {
1263            text += "F3 " + Bundle.getMessage(POWER_STATE_OFF) + "; ";
1264        }
1265        if ((element3 & 0x08) != 0) {
1266            text += "F4 " + Bundle.getMessage(POWER_STATE_ON) + "; ";
1267        } else {
1268            text += "F4 " + Bundle.getMessage(POWER_STATE_OFF) + "; ";
1269        }
1270        if ((element4 & 0x01) != 0) {
1271            text += "F5 " + Bundle.getMessage(POWER_STATE_ON) + "; ";
1272        } else {
1273            text += "F5 " + Bundle.getMessage(POWER_STATE_OFF) + "; ";
1274        }
1275        if ((element4 & 0x02) != 0) {
1276            text += "F6 " + Bundle.getMessage(POWER_STATE_ON) + "; ";
1277        } else {
1278            text += "F6 " + Bundle.getMessage(POWER_STATE_OFF) + "; ";
1279        }
1280        if ((element4 & 0x04) != 0) {
1281            text += "F7 " + Bundle.getMessage(POWER_STATE_ON) + "; ";
1282        } else {
1283            text += "F7 " + Bundle.getMessage(POWER_STATE_OFF) + "; ";
1284        }
1285        if ((element4 & 0x08) != 0) {
1286            text += "F8 " + Bundle.getMessage(POWER_STATE_ON) + "; ";
1287        } else {
1288            text += "F8 " + Bundle.getMessage(POWER_STATE_OFF) + "; ";
1289        }
1290        if ((element4 & 0x10) != 0) {
1291            text += "F9 " + Bundle.getMessage(POWER_STATE_ON) + "; ";
1292        } else {
1293            text += "F9 " + Bundle.getMessage(POWER_STATE_OFF) + "; ";
1294        }
1295        if ((element4 & 0x20) != 0) {
1296            text += "F10 " + Bundle.getMessage(POWER_STATE_ON) + "; ";
1297        } else {
1298            text += "F10 " + Bundle.getMessage(POWER_STATE_OFF) + "; ";
1299        }
1300        if ((element4 & 0x40) != 0) {
1301            text += "F11 " + Bundle.getMessage(POWER_STATE_ON) + "; ";
1302        } else {
1303            text += "F11 " + Bundle.getMessage(POWER_STATE_OFF) + "; ";
1304        }
1305        if ((element4 & 0x80) != 0) {
1306            text += "F12 " + Bundle.getMessage(POWER_STATE_ON) + "; ";
1307        } else {
1308            text += "F12 " + Bundle.getMessage(POWER_STATE_OFF) + "; ";
1309        }
1310        return (text);
1311    }
1312
1313    /**
1314     * Parse the status of functions F13-F28.
1315     *
1316     * @param element3 contains F20,F19,F18,F17,F16,F15,F14,F13
1317     * @param element4 contains F28,F27,F26,F25,F24,F23,F22,F21
1318     * @return readable version of message
1319     */
1320    protected String parseFunctionHighStatus(int element3, int element4) {
1321        String text = "";
1322        if ((element3 & 0x01) != 0) {
1323            text += "F13 " + Bundle.getMessage(POWER_STATE_ON) + "; ";
1324        } else {
1325            text += "F13 " + Bundle.getMessage(POWER_STATE_OFF) + "; ";
1326        }
1327        if ((element3 & 0x02) != 0) {
1328            text += "F14 " + Bundle.getMessage(POWER_STATE_ON) + "; ";
1329        } else {
1330            text += "F14 " + Bundle.getMessage(POWER_STATE_OFF) + "; ";
1331        }
1332        if ((element3 & 0x04) != 0) {
1333            text += "F15 " + Bundle.getMessage(POWER_STATE_ON) + "; ";
1334        } else {
1335            text += "F15 " + Bundle.getMessage(POWER_STATE_OFF) + "; ";
1336        }
1337        if ((element3 & 0x08) != 0) {
1338            text += "F16 " + Bundle.getMessage(POWER_STATE_ON) + "; ";
1339        } else {
1340            text += "F16 " + Bundle.getMessage(POWER_STATE_OFF) + "; ";
1341        }
1342        if ((element3 & 0x10) != 0) {
1343            text += "F17 " + Bundle.getMessage(POWER_STATE_ON) + "; ";
1344        } else {
1345            text += "F17 " + Bundle.getMessage(POWER_STATE_OFF) + "; ";
1346        }
1347        if ((element3 & 0x20) != 0) {
1348            text += "F18 " + Bundle.getMessage(POWER_STATE_ON) + "; ";
1349        } else {
1350            text += "F18 " + Bundle.getMessage(POWER_STATE_OFF) + "; ";
1351        }
1352        if ((element3 & 0x40) != 0) {
1353            text += "F19 " + Bundle.getMessage(POWER_STATE_ON) + "; ";
1354        } else {
1355            text += "F19 " + Bundle.getMessage(POWER_STATE_OFF) + "; ";
1356        }
1357        if ((element3 & 0x80) != 0) {
1358            text += "F20 " + Bundle.getMessage(POWER_STATE_ON) + "; ";
1359        } else {
1360            text += "F20 " + Bundle.getMessage(POWER_STATE_OFF) + "; ";
1361        }
1362        if ((element4 & 0x01) != 0) {
1363            text += "F21 " + Bundle.getMessage(POWER_STATE_ON) + "; ";
1364        } else {
1365            text += "F21 " + Bundle.getMessage(POWER_STATE_OFF) + "; ";
1366        }
1367        if ((element4 & 0x02) != 0) {
1368            text += "F22 " + Bundle.getMessage(POWER_STATE_ON) + "; ";
1369        } else {
1370            text += "F22 " + Bundle.getMessage(POWER_STATE_OFF) + "; ";
1371        }
1372        if ((element4 & 0x04) != 0) {
1373            text += "F23 " + Bundle.getMessage(POWER_STATE_ON) + "; ";
1374        } else {
1375            text += "F23 " + Bundle.getMessage(POWER_STATE_OFF) + "; ";
1376        }
1377        if ((element4 & 0x08) != 0) {
1378            text += "F24 " + Bundle.getMessage(POWER_STATE_ON) + "; ";
1379        } else {
1380            text += "F24 " + Bundle.getMessage(POWER_STATE_OFF) + "; ";
1381        }
1382        if ((element4 & 0x10) != 0) {
1383            text += "F25 " + Bundle.getMessage(POWER_STATE_ON) + "; ";
1384        } else {
1385            text += "F25 " + Bundle.getMessage(POWER_STATE_OFF) + "; ";
1386        }
1387        if ((element4 & 0x20) != 0) {
1388            text += "F26 " + Bundle.getMessage(POWER_STATE_ON) + "; ";
1389        } else {
1390            text += "F26 " + Bundle.getMessage(POWER_STATE_OFF) + "; ";
1391        }
1392        if ((element4 & 0x40) != 0) {
1393            text += "F27 " + Bundle.getMessage(POWER_STATE_ON) + "; ";
1394        } else {
1395            text += "F27 " + Bundle.getMessage(POWER_STATE_OFF) + "; ";
1396        }
1397        if ((element4 & 0x80) != 0) {
1398            text += "F28 " + Bundle.getMessage(POWER_STATE_ON) + "; ";
1399        } else {
1400            text += "F28 " + Bundle.getMessage(POWER_STATE_OFF) + "; ";
1401        }
1402        return (text);
1403    }
1404    /**
1405     * Parse the Momentary status of functions.
1406     *
1407     * @param element3 contains the data byte including F0,F1,F2,F3,F4
1408     * @param element4 contains F12,F11,F10,F9,F8,F7,F6,F5
1409     * @return readable version of message
1410     */
1411    protected String parseFunctionMomentaryStatus(int element3, int element4) {
1412        String text = "";
1413        if ((element3 & 0x10) != 0) {
1414            text += "F0 " + Bundle.getMessage(FUNCTION_MOMENTARY) + "; ";
1415        } else {
1416            text += "F0 " + Bundle.getMessage(FUNCTION_CONTINUOUS) + "; ";
1417        }
1418        if ((element3 & 0x01) != 0) {
1419            text += "F1 " + Bundle.getMessage(FUNCTION_MOMENTARY) + "; ";
1420        } else {
1421            text += "F1 " + Bundle.getMessage(FUNCTION_CONTINUOUS) + "; ";
1422        }
1423        if ((element3 & 0x02) != 0) {
1424            text += "F2 " + Bundle.getMessage(FUNCTION_MOMENTARY) + "; ";
1425        } else {
1426            text += "F2 " + Bundle.getMessage(FUNCTION_CONTINUOUS) + "; ";
1427        }
1428        if ((element3 & 0x04) != 0) {
1429            text += "F3 " + Bundle.getMessage(FUNCTION_MOMENTARY) + "; ";
1430        } else {
1431            text += "F3 " + Bundle.getMessage(FUNCTION_CONTINUOUS) + "; ";
1432        }
1433        if ((element3 & 0x08) != 0) {
1434            text += "F4 " + Bundle.getMessage(FUNCTION_MOMENTARY) + "; ";
1435        } else {
1436            text += "F4 " + Bundle.getMessage(FUNCTION_CONTINUOUS) + "; ";
1437        }
1438        if ((element4 & 0x01) != 0) {
1439            text += "F5 " + Bundle.getMessage(FUNCTION_MOMENTARY) + "; ";
1440        } else {
1441            text += "F5 " + Bundle.getMessage(FUNCTION_CONTINUOUS) + "; ";
1442        }
1443        if ((element4 & 0x02) != 0) {
1444            text += "F6 " + Bundle.getMessage(FUNCTION_MOMENTARY) + "; ";
1445        } else {
1446            text += "F6 " + Bundle.getMessage(FUNCTION_CONTINUOUS) + "; ";
1447        }
1448        if ((element4 & 0x04) != 0) {
1449            text += "F7 " + Bundle.getMessage(FUNCTION_MOMENTARY) + "; ";
1450        } else {
1451            text += "F7 " + Bundle.getMessage(FUNCTION_CONTINUOUS) + "; ";
1452        }
1453        if ((element4 & 0x08) != 0) {
1454            text += "F8 " + Bundle.getMessage(FUNCTION_MOMENTARY) + "; ";
1455        } else {
1456            text += "F8 " + Bundle.getMessage(FUNCTION_CONTINUOUS) + "; ";
1457        }
1458        if ((element4 & 0x10) != 0) {
1459            text += "F9 " + Bundle.getMessage(FUNCTION_MOMENTARY) + "; ";
1460        } else {
1461            text += "F9 " + Bundle.getMessage(FUNCTION_CONTINUOUS) + "; ";
1462        }
1463        if ((element4 & 0x20) != 0) {
1464            text += "F10 " + Bundle.getMessage(FUNCTION_MOMENTARY) + "; ";
1465        } else {
1466            text += "F10 " + Bundle.getMessage(FUNCTION_CONTINUOUS) + "; ";
1467        }
1468        if ((element4 & 0x40) != 0) {
1469            text += "F11 " + Bundle.getMessage(FUNCTION_MOMENTARY) + "; ";
1470        } else {
1471            text += "F11 " + Bundle.getMessage(FUNCTION_CONTINUOUS) + "; ";
1472        }
1473        if ((element4 & 0x80) != 0) {
1474            text += "F12 " + Bundle.getMessage(FUNCTION_MOMENTARY) + "; ";
1475        } else {
1476            text += "F12 " + Bundle.getMessage(FUNCTION_CONTINUOUS) + "; ";
1477        }
1478        return (text);
1479    }
1480
1481    /**
1482     * Parse the Momentary sytatus of functions F13-F28.
1483     *
1484     * @param element3 contains F20,F19,F18,F17,F16,F15,F14,F13
1485     * @param element4 contains F28,F27,F26,F25,F24,F23,F22,F21
1486     * @return readable version of message
1487     */
1488    protected String parseFunctionHighMomentaryStatus(int element3, int element4) {
1489        String text = "";
1490        if ((element3 & 0x01) != 0) {
1491            text += "F13 " + Bundle.getMessage(FUNCTION_MOMENTARY) + "; ";
1492        } else {
1493            text += "F13 " + Bundle.getMessage(FUNCTION_CONTINUOUS) + "; ";
1494        }
1495        if ((element3 & 0x02) != 0) {
1496            text += "F14 " + Bundle.getMessage(FUNCTION_MOMENTARY) + "; ";
1497        } else {
1498            text += "F14 " + Bundle.getMessage(FUNCTION_CONTINUOUS) + "; ";
1499        }
1500        if ((element3 & 0x04) != 0) {
1501            text += "F15 " + Bundle.getMessage(FUNCTION_MOMENTARY) + "; ";
1502        } else {
1503            text += "F15 " + Bundle.getMessage(FUNCTION_CONTINUOUS) + "; ";
1504        }
1505        if ((element3 & 0x08) != 0) {
1506            text += "F16 " + Bundle.getMessage(FUNCTION_MOMENTARY) + "; ";
1507        } else {
1508            text += "F16 " + Bundle.getMessage(FUNCTION_CONTINUOUS) + "; ";
1509        }
1510        if ((element3 & 0x10) != 0) {
1511            text += "F17 " + Bundle.getMessage(FUNCTION_MOMENTARY) + "; ";
1512        } else {
1513            text += "F17 " + Bundle.getMessage(FUNCTION_CONTINUOUS) + "; ";
1514        }
1515        if ((element3 & 0x20) != 0) {
1516            text += "F18 " + Bundle.getMessage(FUNCTION_MOMENTARY) + "; ";
1517        } else {
1518            text += "F18 " + Bundle.getMessage(FUNCTION_CONTINUOUS) + "; ";
1519        }
1520        if ((element3 & 0x40) != 0) {
1521            text += "F19 " + Bundle.getMessage(FUNCTION_MOMENTARY) + "; ";
1522        } else {
1523            text += "F19 " + Bundle.getMessage(FUNCTION_CONTINUOUS) + "; ";
1524        }
1525        if ((element3 & 0x80) != 0) {
1526            text += "F20 " + Bundle.getMessage(FUNCTION_MOMENTARY) + "; ";
1527        } else {
1528            text += "F20 " + Bundle.getMessage(FUNCTION_CONTINUOUS) + "; ";
1529        }
1530        if ((element4 & 0x01) != 0) {
1531            text += "F21 " + Bundle.getMessage(FUNCTION_MOMENTARY) + "; ";
1532        } else {
1533            text += "F21 " + Bundle.getMessage(FUNCTION_CONTINUOUS) + "; ";
1534        }
1535        if ((element4 & 0x02) != 0) {
1536            text += "F22 " + Bundle.getMessage(FUNCTION_MOMENTARY) + "; ";
1537        } else {
1538            text += "F22 " + Bundle.getMessage(FUNCTION_CONTINUOUS) + "; ";
1539        }
1540        if ((element4 & 0x04) != 0) {
1541            text += "F23 " + Bundle.getMessage(FUNCTION_MOMENTARY) + "; ";
1542        } else {
1543            text += "F23 " + Bundle.getMessage(FUNCTION_CONTINUOUS) + "; ";
1544        }
1545        if ((element4 & 0x08) != 0) {
1546            text += "F24 " + Bundle.getMessage(FUNCTION_MOMENTARY) + "; ";
1547        } else {
1548            text += "F24 " + Bundle.getMessage(FUNCTION_CONTINUOUS) + "; ";
1549        }
1550        if ((element4 & 0x10) != 0) {
1551            text += "F25 " + Bundle.getMessage(FUNCTION_MOMENTARY) + "; ";
1552        } else {
1553            text += "F25 " + Bundle.getMessage(FUNCTION_CONTINUOUS) + "; ";
1554        }
1555        if ((element4 & 0x20) != 0) {
1556            text += "F26 " + Bundle.getMessage(FUNCTION_MOMENTARY) + "; ";
1557        } else {
1558            text += "F26 " + Bundle.getMessage(FUNCTION_CONTINUOUS) + "; ";
1559        }
1560        if ((element4 & 0x40) != 0) {
1561            text += "F27 " + Bundle.getMessage(FUNCTION_MOMENTARY) + "; ";
1562        } else {
1563            text += "F27 " + Bundle.getMessage(FUNCTION_CONTINUOUS) + "; ";
1564        }
1565        if ((element4 & 0x80) != 0) {
1566            text += "F28 " + Bundle.getMessage(FUNCTION_MOMENTARY) + "; ";
1567        } else {
1568            text += "F28 " + Bundle.getMessage(FUNCTION_CONTINUOUS) + "; ";
1569        }
1570        return (text);
1571    }
1572
1573}