001package jmri.jmrix.lenz;
002
003import org.reflections.Reflections;
004import org.slf4j.Logger;
005import org.slf4j.LoggerFactory;
006
007import java.lang.reflect.Constructor;
008import java.lang.reflect.InvocationTargetException;
009import java.util.ArrayList;
010import java.util.List;
011import java.util.Optional;
012import java.util.Set;
013import java.util.function.Function;
014import javax.annotation.CheckForNull;
015import javax.annotation.Nonnull;
016
017/**
018 * Represents a single response from the XpressNet.
019 *
020 * @author Paul Bender Copyright (C) 2004
021 *
022 */
023public class XNetReply extends jmri.jmrix.AbstractMRReply {
024
025
026    // unsolicited by message type.
027
028    // Create a new reply.
029    public XNetReply() {
030        super();
031        setBinary(true);
032    }
033
034    // Create a new reply from an existing reply
035    public XNetReply(XNetReply reply) {
036        super(reply);
037        setBinary(true);
038    }
039
040    /**
041     * Create a reply from an XNetMessage.
042     * @param message existing message.
043     */
044    public XNetReply(XNetMessage message) {
045        super();
046        setBinary(true);
047        for (int i = 0; i < message.getNumDataElements(); i++) {
048            setElement(i, message.getElement(i));
049        }
050    }
051
052    /**
053     * Create a reply from a string of hex characters.
054     * @param message hex string of message.
055     */
056    public XNetReply(String message) {
057        super();
058        setBinary(true);
059        // gather bytes in result
060        byte[]  b= jmri.util.StringUtil.bytesFromHexString(message);
061        if (b.length == 0) {
062            // no such thing as a zero-length message
063            _nDataChars = 0;
064            _dataChars = null;
065            return;
066        }
067        _nDataChars = b.length;
068        _dataChars = new int[_nDataChars];
069        for (int i = 0; i < b.length; i++) {
070            setElement(i, ( b[i] & 0xff) );
071        }
072    }
073
074    /**
075     * Get the opcode as a string in hex format.
076     * @return 0x hex string of OpCode.
077     */
078    public String getOpCodeHex() {
079        return "0x" + Integer.toHexString(this.getOpCode());
080    }
081
082    /**
083     * Check whether the message has a valid parity.
084     * @return true if parity valid, else false.
085     */
086    public boolean checkParity() {
087        int len = getNumDataElements();
088        int chksum = 0x00;  /* the seed */
089
090        int loop;
091
092        for (loop = 0; loop < len - 1; loop++) {  // calculate contents for data part
093            chksum ^= getElement(loop);
094        }
095        return ((chksum & 0xFF) == getElement(len - 1));
096    }
097
098    public void setParity() {
099        int len = getNumDataElements();
100        int chksum = 0x00;  /* the seed */
101
102        int loop;
103
104        for (loop = 0; loop < len - 1; loop++) {  // calculate contents for data part
105            chksum ^= getElement(loop);
106        }
107        setElement(len - 1, chksum & 0xFF);
108    }
109
110    /**
111     * Get an integer representation of a BCD value.
112     *
113     * @param n byte in message to convert
114     * @return Integer value of BCD byte.
115     */
116    public Integer getElementBCD(int n) {
117        return Integer.decode(Integer.toHexString(getElement(n)));
118    }
119
120    /**
121     * skipPrefix is not used at this point in time, but is
122     * defined as abstract in AbstractMRReply
123     */
124    @Override
125    protected int skipPrefix(int index) {
126        return -1;
127    }
128
129    // decode messages of a particular form
130
131    /*
132     * The next group of routines are used by Feedback and/or turnout
133     * control code.  These are used in multiple places within the code,
134     * so they appear here.
135     */
136
137    /**
138     * If this is a feedback response message for a turnout, return the address.
139     * Otherwise return -1.
140     *
141     * @return the integer address or -1 if not a turnout message
142     */
143    public int getTurnoutMsgAddr() {
144        if (this.isFeedbackMessage()) {
145            return getTurnoutAddrFromData(
146                    getElement(1),
147                    getElement(2));
148        }
149        return -1;
150    }
151
152    private int getTurnoutAddrFromData(int a1, int a2) {
153        if (getFeedbackMessageType() > 1) {
154            return -1;
155        }
156        int address = (a1 & 0xff) * 4 + 1;
157        if ((a2 & 0x10) != 0) {
158            address += 2;
159        }
160        return address;
161    }
162
163    /**
164     * If this is a feedback broadcast message and the specified startbyte is
165     * the address byte of an addres byte data byte pair for a turnout, return
166     * the address. Otherwise return -1.
167     *
168     * @param startByte address byte of the address byte/data byte pair.
169     * @return the integer address or -1 if not a turnout message
170     */
171    public int getTurnoutMsgAddr(int startByte) {
172        if (this.isFeedbackBroadcastMessage()) {
173            int a1 = this.getElement(startByte);
174            int a2 = this.getElement(startByte + 1);
175            return getTurnoutAddrFromData(a1, a2);
176        } else {
177            return -1;
178        }
179    }
180
181    /**
182     * Parse the feedback message for a turnout, and return the status for the
183     * even or odd half of the nibble (upper or lower part).
184     *
185     * @param turnout <ul>
186     * <li>0 for the even turnout associated with the pair. This is the upper
187     * half of the data nibble asociated with the pair </li>
188     * <li>1 for the odd turnout associated with the pair. This is the lower
189     * half of the data nibble asociated with the pair </li>
190     * </ul>
191     * @return THROWN/CLOSED as defined in {@link jmri.Turnout}
192     */
193    public int getTurnoutStatus(int turnout) {
194        if (this.isFeedbackMessage() && (turnout == 0 || turnout == 1)) {
195            int a2 = this.getElement(2);
196            // fake turnout id, used just internally. Just odd/even matters.
197            return createFeedbackItem(turnout, a2).getTurnoutStatus();
198        }
199        return (-1);
200    }
201
202    /**
203     * Parse the specified address byte/data byte pair in a feedback broadcast
204     * message and see if it is for a turnout. If it is, return the status for
205     * the even or odd half of the nibble (upper or lower part)
206     *
207     * @param startByte address byte of the address byte/data byte pair.
208     * @param turnout   <ul>
209     * <li>0 for the even turnout associated with the pair. This is the upper
210     * half of the data nibble asociated with the pair </li>
211     * <li>1 for the odd turnout associated with the pair. This is the lower
212     * half of the data nibble asociated with the pair </li>
213     * </ul>
214     * @return THROWN/CLOSED as defined in {@link jmri.Turnout}
215     */
216    public int getTurnoutStatus(int startByte, int turnout) {
217        if (this.isFeedbackBroadcastMessage() && (turnout == 0 || turnout == 1)) {
218            int a2 = this.getElement(startByte + 1);
219            // fake turnout id, used just internally. Just odd/even matters.
220            return createFeedbackItem(turnout, a2).getTurnoutStatus();
221        }
222        return (-1);
223    }
224
225    /**
226     * If this is a feedback response message for a feedback encoder, return the
227     * address. Otherwise return -1.
228     *
229     * @return the integer address or -1 if not a feedback message
230     */
231    public int getFeedbackEncoderMsgAddr() {
232        if (this.isFeedbackMessage()) {
233            int a1 = this.getElement(1);
234            int messagetype = this.getFeedbackMessageType();
235            if (messagetype == 2) {
236                // This is a feedback encoder message
237                return (a1 & 0xff);
238            } else {
239                return -1;
240            }
241        } else {
242            return -1;
243        }
244    }
245
246    /**
247     * Returns the number of feedback items in the messages.
248     * For accessory info replies, always returns 1. For broadcast, it returns the
249     * number of feedback pairs. Returns 0 for non-feedback messages.
250     *
251     * @return number of feedback pair items.
252     */
253    public final int getFeedbackMessageItems() {
254        if (isFeedbackMessage()) {
255            return 1;
256        } else if (isFeedbackBroadcastMessage()) {
257            return (this.getElement(0) & 0x0F) / 2;
258        }
259        return 0;
260    }
261
262    /**
263     * If this is a feedback broadcast message and the specified startByte is
264     * the address byte of an address byte/data byte pair for a feedback
265     * encoder, return the address. Otherwise return -1.
266     *
267     * @param startByte address byte of the address byte data byte pair.
268     * @return the integer address or -1 if not a feedback message
269     */
270    public int getFeedbackEncoderMsgAddr(int startByte) {
271        if (this.isFeedbackBroadcastMessage()) {
272            int a1 = this.getElement(startByte);
273            int messagetype = this.getFeedbackMessageType(startByte);
274            if (messagetype == 2) {
275                // This is a feedback encoder message
276                return (a1 & 0xff);
277            } else {
278                return -1;
279            }
280        } else {
281            return -1;
282        }
283    }
284
285    /**
286     * Is this a feedback response message?
287     * @return true if a feedback response, else false.
288     */
289    public boolean isFeedbackMessage() {
290        return (this.getElement(0) == XNetConstants.ACC_INFO_RESPONSE);
291    }
292
293    /**
294     * Is this a feedback broadcast message?
295     * @return true if a feedback broadcast message, else false.
296     */
297    public boolean isFeedbackBroadcastMessage() {
298        return ((this.getElement(0) & 0xF0) == XNetConstants.BC_FEEDBACK);
299    }
300
301    /**
302     * Extract the feedback message type from a feedback message this is the
303     * middle two bits of the upper byte of the second data byte.
304     *
305     * @return message type, values are:
306     * <ul>
307     * <li>0 for a turnout with no feedback</li>
308     * <li>1 for a turnout with feedback</li>
309     * <li>2 for a feedback encoder</li>
310     * <li>3 is reserved by Lenz for future use.</li>
311     * </ul>
312     */
313    public int getFeedbackMessageType() {
314        if (this.isFeedbackMessage()) {
315            int a2 = this.getElement(2);
316            return ((a2 & 0x60) / 32);
317        } else {
318            return -1;
319        }
320    }
321
322    /**
323     * Extract the feedback message type from the data byte of associated with
324     * the specified address byte specified by startByte.
325     * <p>
326     * The return value is the middle two bits of the upper byte of the data
327     * byte of an address byte/data byte pair.
328     *
329     * @param startByte The address byte for this addres byte data byte pair.
330     * @return message type, values are:
331     * <ul>
332     * <li>0 for a turnout with no feedback</li>
333     * <li>1 for a turnout with feedback</li>
334     * <li>2 for a feedback encoder</li>
335     * <li>3 is reserved by Lenz for future use.</li>
336     * </ul>
337     */
338    public int getFeedbackMessageType(int startByte) {
339        if (this.isFeedbackBroadcastMessage()) {
340            int a2 = this.getElement(startByte + 1);
341            return ((a2 & 0x60) / 32);
342        } else {
343            return -1;
344        }
345    }
346
347    public boolean isFeedbackMotionComplete(int startByte) {
348        int messageType = getFeedbackMessageType(startByte);
349        if (messageType == 1) {
350            int a2 = getElement(startByte + 1);
351            return ((a2 & 0x80) != 0x80);
352        }
353        return false;
354    }
355
356    /*
357     * Next we have a few throttle related messages
358     */
359
360    /**
361     * If this is a throttle-type message, return address.
362     * Otherwise return -1.
363     * <p>
364     * Note we only identify the command now;
365     * the response to a request for status is not yet seen here.
366     * @return address if throttle-type message, else -1.
367     */
368    public int getThrottleMsgAddr() {
369        if (this.isThrottleMessage()) {
370            int a1 = this.getElement(2);
371            int a2 = this.getElement(3);
372            if (a1 == 0) {
373                return (a2);
374            } else {
375                return (((a1 * 256) & 0xFF00) + (a2 & 0xFF) - 0xC000);
376            }
377        } else {
378            return -1;
379        }
380    }
381
382    /**
383     * Is this a throttle message?
384     * @return true if throttle message. else false.
385     */
386    public boolean isThrottleMessage() {
387        int message = this.getElement(0);
388        return (message == XNetConstants.LOCO_INFO_NORMAL_UNIT
389                || message == XNetConstants.LOCO_INFO_RESPONSE
390                || message == XNetConstants.LOCO_INFO_MUED_UNIT
391                || message == XNetConstants.LOCO_INFO_MU_ADDRESS
392                || message == XNetConstants.LOCO_INFO_DH_UNIT
393                || message == XNetConstants.LOCO_AVAILABLE_V1
394                || message == XNetConstants.LOCO_AVAILABLE_V2
395                || message == XNetConstants.LOCO_NOT_AVAILABLE_V1
396                || message == XNetConstants.LOCO_NOT_AVAILABLE_V2);
397    }
398
399    /**
400     * Does this message indicate the locomotive has been taken over by another
401     * device?
402     * @return true if take over message, else false.
403     */
404    public boolean isThrottleTakenOverMessage() {
405        return (this.getElement(0) == XNetConstants.LOCO_INFO_RESPONSE
406                && this.getElement(1) == XNetConstants.LOCO_NOT_AVAILABLE);
407    }
408
409    /**
410     * Is this a consist message?
411     * @return true if consist message, else false.
412     */
413    public boolean isConsistMessage() {
414        int message = this.getElement(0);
415        return (message == XNetConstants.LOCO_MU_DH_ERROR
416                || message == XNetConstants.LOCO_DH_INFO_V1
417                || message == XNetConstants.LOCO_DH_INFO_V2);
418    }
419
420    /*
421     * Finally, we have some commonly used routines that are used for
422     * checking specific, generic, response messages.
423     */
424
425    /**
426     * In the interest of code reuse, the following function checks to see
427     * if an XpressNet Message is the OK message (01 04 05).
428     * @return true if an OK message, else false.
429     */
430    public boolean isOkMessage() {
431        return (this.getElement(0) == XNetConstants.LI_MESSAGE_RESPONSE_HEADER
432                && this.getElement(1) == XNetConstants.LI_MESSAGE_RESPONSE_SEND_SUCCESS);
433    }
434
435    /**
436     * In the interest of code reuse, the following function checks to see
437     * if an XpressNet Message is the timeslot restored message (01 07 06).
438     * @return true if a time-slot restored message.
439     */
440    public boolean isTimeSlotRestored() {
441        return (this.getElement(0) == XNetConstants.LI_MESSAGE_RESPONSE_HEADER
442                && this.getElement(1) == XNetConstants.LIUSB_TIMESLOT_RESTORED);
443    }
444
445    /**
446     * In the interest of code reuse, the following function checks to see
447     * if an XpressNet Message is the Command Station no longer providing a
448     * timeslot message (01 05 04).
449     * @return true if a time-slot revoked message, else false.
450     */
451    public boolean isTimeSlotRevoked() {
452        return (this.getElement(0) == XNetConstants.LI_MESSAGE_RESPONSE_HEADER
453                && this.getElement(1) == XNetConstants.LI_MESSAGE_RESPONSE_TIMESLOT_ERROR);
454    }
455
456    /**
457     * In the interest of code reuse, the following function checks to see
458     * if an XpressNet Message is the Command Station Busy message (61 81 e3).
459     * @return true if is a CS Busy message, else false.
460     */
461    public boolean isCSBusyMessage() {
462        return (this.getElement(0) == XNetConstants.CS_INFO
463                && this.getElement(1) == XNetConstants.CS_BUSY);
464    }
465
466
467    /**
468     * In the interest of code reuse, the following function checks to see
469     * if an XpressNet Message is the Command Station Transfer Error
470     * message (61 80 e1).
471     * @return if CS Transfer error, else false.
472     */
473    public boolean isCSTransferError() {
474        return (this.getElement(0) == XNetConstants.CS_INFO
475                && this.getElement(1) == XNetConstants.CS_TRANSFER_ERROR);
476    }
477
478    /**
479     * In the interest of code reuse, the following function checks to see
480     * if an XpressNet Message is the not supported Error
481     * message (61 82 e3).
482     * @return true if unsupported error, else false.
483     */
484    public boolean isUnsupportedError() {
485        return (this.getElement(0) == XNetConstants.CS_INFO
486                && this.getElement(1) == XNetConstants.CS_NOT_SUPPORTED);
487    }
488
489    /**
490     * In the interest of code reuse, the following function checks to see
491     * if an XpressNet Message is a communications error message.
492     * <p>
493     * The errors handled are:
494     *  01 01 00  -- Error between interface and the PC
495     *  01 02 03  -- Error between interface and the Command Station
496     *  01 03 02  -- Unknown Communications Error
497     *  01 06 07  -- LI10x Buffer Overflow
498     *  01 0A 0B  -- LIUSB only. Request resend of data.
499     * @return true if comm error message, else false.
500     */
501    public boolean isCommErrorMessage() {
502        return (this.getElement(0) == XNetConstants.LI_MESSAGE_RESPONSE_HEADER
503                && (this.getElement(1) == XNetConstants.LI_MESSAGE_RESPONSE_UNKNOWN_DATA_ERROR
504                || this.getElement(1) == XNetConstants.LI_MESSAGE_RESPONSE_CS_DATA_ERROR
505                || this.getElement(1) == XNetConstants.LI_MESSAGE_RESPONSE_PC_DATA_ERROR
506                || this.getElement(1) == XNetConstants.LIUSB_RETRANSMIT_REQUEST
507                || this.getElement(1) == XNetConstants.LI_MESSAGE_RESPONSE_BUFFER_OVERFLOW)
508                || this.isTimeSlotErrorMessage());
509    }
510
511    /**
512     * In the interest of code reuse, the following function checks to see
513     * if an XpressNet Message is a communications error message.
514     * <p>
515     * The errors handled are:
516     *  01 05 04  -- Timeslot Error
517     *  01 07 06  -- Timeslot Restored
518     *  01 08 09  -- Data sent while there is no Timeslot
519     * @return true if time slot error, else false.
520     */
521    public boolean isTimeSlotErrorMessage() {
522        return (this.getElement(0) == XNetConstants.LI_MESSAGE_RESPONSE_HEADER
523                && (this.getElement(1) == XNetConstants.LIUSB_REQUEST_SENT_WHILE_NO_TIMESLOT
524                || this.getElement(1) == XNetConstants.LIUSB_TIMESLOT_RESTORED
525                || this.getElement(1) == XNetConstants.LI_MESSAGE_RESPONSE_TIMESLOT_ERROR));
526    }
527
528
529    /**
530     * Is this message a service mode response?
531     * @return true if a service mode response, else false.
532     */
533    public boolean isServiceModeResponse() {
534        return (getElement(0) == XNetConstants.CS_SERVICE_MODE_RESPONSE
535                && (getElement(1) == XNetConstants.CS_SERVICE_DIRECT_RESPONSE
536                || getElement(1) == (XNetConstants.CS_SERVICE_DIRECT_RESPONSE + 1)
537                || getElement(1) == (XNetConstants.CS_SERVICE_DIRECT_RESPONSE + 2)
538                || getElement(1) == (XNetConstants.CS_SERVICE_DIRECT_RESPONSE + 3)
539                || getElement(1) == XNetConstants.CS_SERVICE_REG_PAGE_RESPONSE));
540    }
541
542    /**
543     * Is this message a register or paged mode programming response?
544     * @return true if register or paged mode programming response, else false.
545     */
546    public boolean isPagedModeResponse() {
547        return (getElement(0) == XNetConstants.CS_SERVICE_MODE_RESPONSE
548                && getElement(1) == XNetConstants.CS_SERVICE_REG_PAGE_RESPONSE);
549    }
550
551    /**
552     * Is this message a direct CV mode programming response?
553     * @return true if direct CV mode programming response, else false.
554     */
555    public boolean isDirectModeResponse() {
556        return (getElement(0) == XNetConstants.CS_SERVICE_MODE_RESPONSE
557                && (getElement(1) == XNetConstants.CS_SERVICE_DIRECT_RESPONSE
558                || getElement(1) == (XNetConstants.CS_SERVICE_DIRECT_RESPONSE + 1)
559                || getElement(1) == (XNetConstants.CS_SERVICE_DIRECT_RESPONSE + 2)
560                || getElement(1) == (XNetConstants.CS_SERVICE_DIRECT_RESPONSE + 3)));
561    }
562
563    /**
564     * @return the CV value associated with a service mode reply
565     * return -1 if not a service mode message.
566     */
567    public int getServiceModeCVNumber() {
568        int cv = -1;
569        if (isServiceModeResponse()) {
570            if ((getElement(1) & XNetConstants.CS_SERVICE_DIRECT_RESPONSE) == XNetConstants.CS_SERVICE_DIRECT_RESPONSE) {
571                cv = (getElement(1) - XNetConstants.CS_SERVICE_DIRECT_RESPONSE) * 256 + getElement(2);
572            } else {
573                cv = getElement(2);
574            }
575        }
576        return (cv);
577    }
578
579    /**
580     * @return the value returned by the DCC system associated with a
581     * service mode reply.
582     * return -1 if not a service mode message.
583     */
584    public int getServiceModeCVValue() {
585        int value = -1;
586        if (isServiceModeResponse()) {
587            value = getElement(3);
588        }
589        return (value);
590    }
591
592    /**
593     * @return true if the message is an error message indicating
594     * we should retransmit.
595     */
596    @Override
597    public boolean isRetransmittableErrorMsg() {
598        return (this.isCSBusyMessage()
599                || this.isCommErrorMessage()
600                || this.isCSTransferError());
601    }
602
603    /**
604     * @return true if the message is an unsolicited message
605     */
606    @Override
607    public boolean isUnsolicited() {
608        // The message may be set as an unsolicited message else where
609        // or it may be classified as unsolicited based on the type of
610        // message received.
611        // NOTE: The feedback messages may be received in either solicited
612        // or unsolicited form.  requesting code can mark the reply as solicited
613        // by calling the resetUnsolicited function.
614        return (super.isUnsolicited()
615                || this.isThrottleTakenOverMessage());
616    }
617
618    /**
619     * Mask to identify a turnout feedback + correct nibble. Turnout types differ in
620     * 6th bit, so it's left out (is variable).
621     */
622    private static final int FEEDBACK_TURNOUT_MASK = 0b0101_0000;
623
624    /**
625     * Mask to identify a feedback module + correct nibble. Turnout modules have
626     * type exactly 2.
627     */
628    private static final int FEEDBACK_MODULE_MASK  = 0b0111_0000;
629
630    /**
631     * The value of "feedback module" type.
632     */
633    private static final int FEEDBACK_TYPE_FBMODULE = 0b0100_0000;
634
635    /**
636     * Bit that indicates the higher nibble in module or turnout feedback
637     */
638    private static final int FEEDBACK_HIGH_NIBBLE = 0b0001_0000;
639
640    private int findFeedbackData(int baseAddress, int selector, int mask) {
641        if (isFeedbackMessage()) {
642            // shorctcut for single-item msg
643            int data = getElement(2);
644            if (getElement(1) == baseAddress &&
645                (data & mask) == selector) {
646                return data;
647            }
648        } else {
649            int start = 1;
650            for (int cnt = getFeedbackMessageItems(); cnt > 0; cnt--, start += 2) {
651                int data = getElement(start + 1);
652                if (getElement(start) == baseAddress &&
653                    (data & mask) == selector) {
654                    return data;
655                }
656            }
657        }
658        return -1;
659    }
660
661    /**
662     * Returns value of the given feedback module bit. Returns {@link Optional}
663     * that is non-empty, if the feedback was present. The Optional's value indicates the
664     * feedback state.
665     *
666     * @param sensorNumber the sensor bit ID
667     * @return optional sensor state.
668     */
669    @CheckForNull
670    public Boolean selectModuleFeedback(int sensorNumber) {
671        if (!isFeedbackBroadcastMessage() || sensorNumber == 0 || sensorNumber >= 1024) {
672            return null;
673        }
674        // feedback address directly addresses 8-bit module, XpressNet spec 3.0:2.1.11.
675        int s = sensorNumber - 1;
676        int baseAddress = (s / 8);
677        int selector2 = (s & 0x04) != 0 ?
678                FEEDBACK_TYPE_FBMODULE | FEEDBACK_HIGH_NIBBLE :
679                FEEDBACK_TYPE_FBMODULE;
680        int res = findFeedbackData(baseAddress, selector2, FEEDBACK_MODULE_MASK);
681        return res == -1 ? null :
682                (res & (1 << (s % 4))) > 0;
683    }
684
685    /**
686     * Calls processor for turnout's feedback, returns the processor's outcome.
687     * Searches for the turnout feedback for the given accessory. If found,
688     * runs a processor on the feedback item, and returns its Boolean result.
689     * <p>
690     * Returns {@code false}, if matching feedback is not found.
691     * @param accessoryNumber the turnout number
692     * @param proc the processor
693     * @return {@code false} if feedback was not found, or a result of {@code proc()}.
694     */
695    public boolean onTurnoutFeedback(int accessoryNumber, Function<FeedbackItem, Boolean> proc) {
696        return selectTurnoutFeedback(accessoryNumber).map(proc).orElse(false);
697    }
698
699    /**
700     * Selects a matching turnout feedback. Finds turnout feedback for the given {@code accessoryNumber}.
701     * Returns an encapsulated feedback, that can be inspected. If no matching feedback is
702     * present, returns empty {@link Optional}.
703     * @param accessoryNumber the turnout number
704     * @return optional feedback item.
705     */
706    @Nonnull
707    public Optional<FeedbackItem> selectTurnoutFeedback(int accessoryNumber) {
708        // shortcut for single-item messages.
709        if (!isFeedbackBroadcastMessage() || accessoryNumber <= 0 || accessoryNumber >= 1024) {
710            return Optional.empty();
711        }
712        int a = accessoryNumber - 1;
713        int base = (a / 4);
714        // the mask makes the turnout feedback type bit irrelevant
715        int selector2 = (a & 0x02) != 0 ? FEEDBACK_HIGH_NIBBLE : 0;
716        int r = findFeedbackData(base, selector2, FEEDBACK_TURNOUT_MASK);
717        if (r == -1) {
718            return Optional.empty();
719        }
720        FeedbackItem item = new FeedbackItem(this, accessoryNumber, r);
721        return Optional.of(item);
722    }
723
724    protected final FeedbackItem createFeedbackItem(int n, int d) {
725        return new FeedbackItem(this, n, d);
726    }
727
728    private static final List<XPressNetMessageFormatter> formatterList = new ArrayList<>();
729    /**
730     * @return a string representation of the reply suitable for display in the
731     * XpressNet monitor.
732     */
733    @Override
734    public String toMonitorString() {
735
736        if (formatterList.isEmpty()) {
737            try {
738                Reflections reflections = new Reflections("jmri.jmrix");
739                Set<Class<? extends XPressNetMessageFormatter>> f = reflections.getSubTypesOf(XPressNetMessageFormatter.class);
740                for (Class<?> c : f) {
741                    log.debug("Found formatter: {}", f.getClass().getName());
742                    Constructor<?> ctor = c.getConstructor();
743                    formatterList.add((XPressNetMessageFormatter) ctor.newInstance());
744                }
745            } catch (NoSuchMethodException | SecurityException | InstantiationException |
746                     IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
747                log.error("Error instantiating formatter", e);
748            }
749        }
750
751        return formatterList.stream().filter(f -> f.handlesMessage(this)).findFirst().map(f -> f.formatMessage(this)).orElse(this.toString());
752    }
753
754    // initialize logging
755    private static final Logger log = LoggerFactory.getLogger(XNetReply.class);
756
757}