001package jmri.jmrix.loconet.uhlenbrock;
002
003import jmri.jmrix.loconet.LnConstants;
004import jmri.jmrix.loconet.LocoNetMessage;
005import org.slf4j.Logger;
006import org.slf4j.LoggerFactory;
007
008import java.util.Locale;
009import java.util.Objects;
010
011/**
012 * Supporting class for Uhlenbrock LocoNet LNCV Programming and Direct Format messaging.
013 * Structure adapted from {@link jmri.jmrix.loconet.lnsvf2.LnSv2MessageContents}
014 * 
015 * Some of the message formats used in this class are Copyright Uhlenbrock.de
016 * and used with permission as part of the JMRI project. That permission does
017 * not extend to uses in other software products. If you wish to use this code,
018 * algorithm or these message formats outside of JMRI, please contact Uhlenbrock.
019 *
020 * @author Egbert Broerse Copyright (C) 2020, 2021
021 */
022public class LncvMessageContents {
023    private final int opc;
024    private final int src;
025    private final int dst_l;
026    private final int dst_h;
027    private final int dst;
028    private final int cmd;
029    private final int art_l; // D1
030    private final int art_h; // D2
031    private final int art;
032    private final String sArt;
033    private final int cvn_l; // D3
034    private final int cvn_h; // D4
035    private final int cvn;
036    private final String sCvn;
037    private final int mod_l; // D5
038    private final int mod_h; // D6
039    private final int mod;
040    private final String sMod;
041    private final int cmd_data; // D7
042    private final LncvCommand command;
043
044    // LocoNet "LNCV format" helper definitions: length byte value for LNCV message
045    public final static int LNCV_LENGTH_ELEMENT_VALUE = 0x0f;
046    public final static int LNCV_LNMODULE_VALUE = 0x05;
047    public final static int LNCV_CS_SRC_VALUE = 0x01;
048    public final static int LNCV_PC_SRC_VALUE = 0x08;
049    public final static int LNCV_CSDEST_VALUE = 0x4b49;
050    public final static int LNCV_ALL = 0xffff; // decimal 65535
051    public final static int LNCV_ALL_MASK = 0xff00; // decimal 65535
052    // the valid range for module addresses (CV0) as per the LNCV spec.
053    public final static int LNCV_MIN_MODULEADDR = 0;
054    public final static int LNCV_MAX_MODULEADDR = 65534;
055
056    // LocoNet "LNCV format" helper definitions: indexes into the LocoNet message
057    public final static int LNCV_LENGTH_ELEMENT_INDEX = 1;
058    public final static int LNCV_SRC_ELEMENT_INDEX = 2;
059    public final static int LNCV_DST_L_ELEMENT_INDEX = 3;
060    public final static int LNCV_DST_H_ELEMENT_INDEX = 4;
061    public final static int LNCV_CMD_ELEMENT_INDEX = 5;
062    public final static int PXCT1_ELEMENT_INDEX = 6;
063    public final static int LNCV_ART_L_ELEMENT_INDEX = 7;
064    public final static int LNCV_ART_H_ELEMENT_INDEX = 8;
065    public final static int LNCV_CVN_L_ELEMENT_INDEX = 9;
066    public final static int LNCV_CVN_H_ELEMENT_INDEX = 10;
067    public final static int LNCV_MOD_L_ELEMENT_INDEX = 11; // val_l reply is in same positions as mod_l read
068    public final static int LNCV_MOD_H_ELEMENT_INDEX = 12; // val_h reply is in same positions as mod_h read
069    public final static int LNCV_CMDDATA_ELEMENT_INDEX = 13;
070    // Checksum = index 14
071
072    //  helpers for decoding CV format 2 messages (no other OCP_PEER_XFER messages with length 0x0f)
073    public final static int LNCV_SRC_ELEMENT_MASK = 0x7f;
074    public final static int PXCT1_ELEMENT_VALIDITY_CHECK_MASK = 0x70;
075    public final static int LNCV_ART_L_ARTL7_CHECK_MASK = 0x01;
076    public final static int LNCV_ART_H_ARTH7_CHECK_MASK = 0x02;
077    public final static int LNCV_CVN_L_CVNL7_CHECK_MASK = 0x04;
078    public final static int LNCV_CVN_H_CVNH7_CHECK_MASK = 0x08;
079    public final static int LNCV_MOD_L_MODL7_CHECK_MASK = 0x10;
080    public final static int LNCV_MOD_H_MODH7_CHECK_MASK = 0x20;
081    public final static int LNCV_CMDDATA_DAT7_CHECK_MASK = 0x40;
082
083    // LocoNet "LNCV format" helper definitions for data
084    //    public final static int LNCV_DATA_START = 0x00;
085    //    public final static int LNCV_DATA_END = 0x40;
086    public final static int LNCV_DATA_PROFF_MASK = 0x40;
087    public final static int LNCV_DATA_PRON_MASK = 0x80;
088    public final static int LNCV_DATA_LED1_MASK = 0xff;
089    public final static int LNCV_DATA_LED2_MASK = 0xfe;
090    public final static int LNCV_DATA_RO_MASK = 0x01;
091
092    // helpers for decoding LNCV_CMD
093    public final static int LNCV_CMD_WRITE = 0x20;
094    public final static int LNCV_CMD_READ = 0x21;
095    public final static int LNCV_CMD_READ_REPLY = 0x1f; // reply to both LNCV_CMD_READ and ENTER_PROG_MOD (in which case CV0 VAL = MOD)
096    // reply to LNCV_CMD_WRITE = LACK, already defined as general LocoNet message type
097
098
099    /**
100     * Create a new LncvMessageContents object from a LocoNet message.
101     *
102     * @param m LocoNet message containing an LNCV Programming Format message
103     * @throws IllegalArgumentException if the LocoNet message is not a valid, supported LNCV Programming Format
104     *                                  message
105     */
106    public LncvMessageContents(LocoNetMessage m) throws IllegalArgumentException {
107
108        //log.debug("interpreting a LocoNet message - may be an LNCV message");  // NOI18N
109        if (!isSupportedLncvMessage(m)) {
110            //log.debug("interpreting a LocoNet message - is NOT an LNCV message");   // NOI18N
111            throw new IllegalArgumentException("LocoNet message is not an LNCV message"); // NOI18N
112        }
113        this.command = extractMessageType(m);
114
115        opc = m.getOpCode();
116        src = m.getElement(LNCV_SRC_ELEMENT_INDEX);
117
118        dst_l = m.getElement(LNCV_DST_L_ELEMENT_INDEX);
119        dst_h = m.getElement(LNCV_DST_H_ELEMENT_INDEX);
120        dst = dst_l + (256 * dst_h);
121        log.debug("src={}, dst={}{}", src, dst, (dst == 19273 ? "=IK" : "")); // must use vars for CI
122
123        cmd = m.getElement(LNCV_CMD_ELEMENT_INDEX);
124
125        int pxct1 = m.getElement(PXCT1_ELEMENT_INDEX);
126        String svx1bin = String.format("%8s", Integer.toBinaryString(pxct1)).replace(' ', '0');
127        log.debug("PXCT1 HIBITS = {}", svx1bin);
128
129        art_l = m.getElement(LNCV_ART_L_ELEMENT_INDEX) + (((pxct1 & LNCV_ART_L_ARTL7_CHECK_MASK) == LNCV_ART_L_ARTL7_CHECK_MASK) ? 0x80 : 0);
130        art_h = m.getElement(LNCV_ART_H_ELEMENT_INDEX) + (((pxct1 & LNCV_ART_H_ARTH7_CHECK_MASK) == LNCV_ART_H_ARTH7_CHECK_MASK) ? 0x80 : 0);
131        art = art_l + (256 * art_h);
132        sArt = art + "";
133
134        cvn_l = m.getElement(LNCV_CVN_L_ELEMENT_INDEX) + (((pxct1 & LNCV_CVN_L_CVNL7_CHECK_MASK) == LNCV_CVN_L_CVNL7_CHECK_MASK) ? 0x80 : 0);
135        cvn_h = m.getElement(LNCV_CVN_H_ELEMENT_INDEX) + (((pxct1 & LNCV_CVN_H_CVNH7_CHECK_MASK) == LNCV_CVN_H_CVNH7_CHECK_MASK) ? 0x80 : 0);
136        cvn = cvn_l + (256 * cvn_h);
137        sCvn = cvn + "";
138
139        mod_l = m.getElement(LNCV_MOD_L_ELEMENT_INDEX) + (((pxct1 & LNCV_MOD_L_MODL7_CHECK_MASK) == LNCV_MOD_L_MODL7_CHECK_MASK) ? 0x80 : 0);
140        mod_h = m.getElement(LNCV_MOD_H_ELEMENT_INDEX) + (((pxct1 & LNCV_MOD_H_MODH7_CHECK_MASK) == LNCV_MOD_H_MODH7_CHECK_MASK) ? 0x80 : 0);
141        mod = mod_l + (256 * mod_h);
142        sMod = mod + "";
143
144        cmd_data = m.getElement(LNCV_CMDDATA_ELEMENT_INDEX) + (((pxct1 & LNCV_CMDDATA_DAT7_CHECK_MASK) == LNCV_CMDDATA_DAT7_CHECK_MASK) ? 0x80 : 0);
145    }
146
147    /**
148     * Check a LocoNet message to determine if it is a valid LNCV Programming Format message.
149     *
150     * @param m LocoNet message to check
151     * @return true if LocoNet message m is a supported LNCV Programming Format message, else false.
152     */
153    public static boolean isSupportedLncvMessage(LocoNetMessage m) {
154        // must be OPC_PEER_XFER or OPC_IMM_PACKET opcode
155        if ((m.getOpCode() != LnConstants.OPC_PEER_XFER) && (m.getOpCode() != LnConstants.OPC_IMM_PACKET)) {
156            //log.debug("cannot be LNCV message because not OPC_PEER_XFER (0xe5) or OPC_IMM_PACKET (0xed)");  // NOI18N
157            return false;
158        }
159
160        // length must be 0x0f
161        if (m.getElement(1) != LNCV_LENGTH_ELEMENT_VALUE) {
162            //log.debug("cannot be LNCV message because not length 0x0f");  // NOI18N
163            return false;
164        }
165
166        // <SRC_ELEMENT> must be correct
167        if ((m.getElement(LNCV_SRC_ELEMENT_INDEX) != LNCV_CS_SRC_VALUE) && (m.getElement(LNCV_SRC_ELEMENT_INDEX) != LNCV_LNMODULE_VALUE) && (m.getElement(LNCV_SRC_ELEMENT_INDEX) != LNCV_PC_SRC_VALUE)) {
168            //log.debug("cannot be LNCV message because Source not correct");  // NOI18N
169            return false;
170        }
171
172        // "command_data" identifier must be correct. handled via Enum
173        // check the (compound) command element
174        int msgData = m.getElement(LNCV_CMDDATA_ELEMENT_INDEX) | (((m.getElement(PXCT1_ELEMENT_INDEX) & LNCV_CMDDATA_DAT7_CHECK_MASK) == LNCV_CMDDATA_DAT7_CHECK_MASK) ? 0x80 : 0);
175        return isSupportedLncvCommand(m.getElement(LNCV_CMD_ELEMENT_INDEX), m.getOpCode(), msgData);
176    }
177
178    /**
179     * Compare reply message against a specific LNCV Programming Format message type.
180     *
181     * @param m  LocoNet message to be verified as an LNCV Programming Format message with the specified
182     *           &lt;LNCV_CMD&gt; value
183     * @param lncvCmd LNCV Programming Format command to check against
184     * @return true if message is an LNCV Programming Format message of the specified &lt;LNCV_CMD&gt;, else false.
185     */
186    public static boolean isLnMessageASpecificLncvCommand(LocoNetMessage m, LncvCommand lncvCmd) {
187        if (!isSupportedLncvMessage(m)) {
188            log.debug("rejected in isLnMessageASpecificLncvCommand");
189            return false;
190        }
191        // compare the <LNCV_CMD> value against cvCmd
192        return Objects.equals(extractMessageType(m), lncvCmd);
193    }
194
195    /**
196     * Interpret a LocoNet message to determine its LNCV compound Programming Format.
197     * If the message is not an LNCV Programming/Direct Format message, returns null.
198     *
199     * @param m LocoNet message containing LNCV Programming Format message
200     * @return LncvCommand found in the LNCV Programming Format message or null if not found
201     */
202    public static LncvCommand extractMessageType(LocoNetMessage m) {
203        if (isSupportedLncvMessage(m)) {
204            int msgCmd = m.getElement(LNCV_CMD_ELEMENT_INDEX);
205            int msgData = m.getElement(LNCV_CMDDATA_ELEMENT_INDEX) | (((m.getElement(PXCT1_ELEMENT_INDEX) & LNCV_CMDDATA_DAT7_CHECK_MASK) == LNCV_CMDDATA_DAT7_CHECK_MASK) ? 0x80 : 0);
206            //log.debug("msgData = {}", msgData);
207            for (LncvCommand c : LncvCommand.values()) {
208                if (c.matches(msgCmd, m.getOpCode(), msgData)) {
209                    //log.debug("LncvCommand match found");  // NOI18N
210                    return c;
211                }
212            }
213        }
214        return null;
215    }
216
217    /**
218     * Interpret the LNCV Programming Format message into a human-readable string.
219     *
220     * @return String containing a human-readable version of the LNCV Programming Format message
221     */
222    @Override
223    public String toString() {
224        Locale l = Locale.getDefault();
225        return LncvMessageContents.this.toString(l);
226    }
227
228    /**
229     * Interpret the LNCV Programming Format message into a human-readable string.
230     *
231     * @param locale locale to use for the human-readable string
232     * @return String containing a human-readable version of the LNCV Programming Format message, in the language
233     * specified by the Locale, if the properties have been translated to that Locale, else in the default English
234     * language.
235     */
236    public String toString(Locale locale) {
237        String returnString;
238        //log.debug("interpreting an LNCV message - simple cmd is {}", cmd);  // NOI18N
239
240        switch (this.command) {
241            case LNCV_PROG_START:
242                if ((art & LNCV_ALL_MASK) == LNCV_ALL_MASK) {
243                    returnString = Bundle.getMessage(locale, "LNCV_ALL_PROG_START_INTERPRETED");
244                } else if ((mod & LNCV_ALL_MASK) == LNCV_ALL_MASK) {
245                    returnString = Bundle.getMessage(locale, "LNCV_ART_PROG_START_INTERPRETED", sArt);
246                } else {
247                    returnString = Bundle.getMessage(locale, "LNCV_MOD_PROG_START_INTERPRETED", sArt, sMod);
248                }
249                break;
250            case LNCV_PROG_END:
251                if ((art & LNCV_ALL_MASK) == LNCV_ALL_MASK) {
252                    returnString = Bundle.getMessage(locale, "LNCV_ALL_PROG_END_INTERPRETED");
253                } else if ((mod & LNCV_ALL_MASK) == LNCV_ALL_MASK) {
254                    returnString = Bundle.getMessage(locale, "LNCV_ART_PROG_END_INTERPRETED", sArt);
255                } else {
256                    returnString = Bundle.getMessage(locale, "LNCV_MOD_PROG_END_INTERPRETED", sArt, sMod);
257                }
258                break;
259            case LNCV_WRITE: // mod positions store CV value in ReadReply
260                returnString = Bundle.getMessage(locale, "LNCV_WRITE_INTERPRETED", sArt, sCvn, sMod);
261                break;
262            case LNCV_READ:
263                // read = module prog start
264                returnString = Bundle.getMessage(locale, "LNCV_READ_INTERPRETED", sArt, sMod, sCvn);
265                break;
266            case LNCV_READ_REPLY: // mod positions store CV value in ReadReply
267            case LNCV_READ_REPLY2: // for Digikeijs DK5088RC not following specs? experimental EBR
268                returnString = Bundle.getMessage(locale, "LNCV_READ_REPLY_INTERPRETED", sArt, sCvn, sMod);
269                break;
270            case LNCV_DIRECT_LED1: // CV position contains module address, Value position contains LED 0-15 on/off
271                returnString = Bundle.getMessage(locale, "LNCV_DIRECT_INTERPRETED", "1", toBinString(mod), sCvn);
272                break;
273            case LNCV_DIRECT_LED2: // CV position contains module address, Value position contains LED 16-31 on/off
274                returnString = Bundle.getMessage(locale, "LNCV_DIRECT_INTERPRETED", "2", toBinString(mod), sCvn);
275                //to16Bits(cvn, true));
276                break;
277            case LNCV_DIRECT_REPLY: // CV position contains module address, value position = Button on/off message
278                returnString = Bundle.getMessage(locale, "LNCV_DIRECT_REPLY_INTERPRETED", sCvn, sMod);
279                break;
280            default:
281                return Bundle.getMessage(locale, "LNCV_UNDEFINED_MESSAGE") + "\n";
282        }
283
284        return returnString + "\n"; // NOI18N
285    }
286
287    /**
288     * Convert binary integer to "1010" representation string.
289     *
290     * @param bin integer to convert to binary display string
291     */
292    private String toBinString(int bin) {
293        return String.format("%8s", Integer.toBinaryString(bin)).replace(' ', '0');
294    }
295
296
297    /**
298     * Check set of parameters against compound {@link LncvCommand} enum set.
299     *
300     * @param command LNCV CMD value
301     * @param opc     OPC value
302     * @param cmdData LNCV cmdData value
303     * @return true if the possibleCmd value is one of the supported (simple) LNCV Programming Format commands
304     */
305    public static boolean isSupportedLncvCommand(int command, int opc, int cmdData) {
306        //log.debug("CMD = {}-{}-{}", command, opc, cmdData);
307        for (LncvCommand commandToCheck : LncvCommand.values()) {
308            if (commandToCheck.matches(command, opc, cmdData)) {
309                return true;
310            }
311        }
312        return false;
313    }
314
315    /**
316     * Confirm a message corresponds with a valid (known) LNCV Programming Format command.
317     *
318     * @return true if the LNCV message specifies a valid (known) LNCV Programming Format command.
319     */
320    public boolean isSupportedLncvCommand() {
321        return isSupportedLncvCommand(cmd, opc, cmd_data);
322    }
323
324    /**
325     * @return true if the LNCV message is an LNCV ReadReply message
326     */
327    public boolean isSupportedLncvReadReply() {
328        return (cmd == LNCV_CMD_READ_REPLY);
329    }
330
331    /**
332     * Create a LocoNet message containing an LNCV Programming Format message.
333     *
334     * @param opc         Opcode (&lt;OPC&gt;), see LnConstants
335     * @param source      source device (&lt;SRC&gt;)
336     * @param destination destination address (for &lt;DST_L&gt; and &lt;DST_H&gt;)
337     * @param command     LNCV Programming simple command (for &lt;LNCV_CMD&gt;), part of
338     *                    complex command {@link LncvCommand}
339     * @param articleNum  manufacturer's hardware class/article code as per specs (4 decimal digits)
340     * @param cvNum       CV number (for &lt;LNCV_CVN_L&gt; and &lt;LNCV_CVN_H&gt;)
341     * @param moduleNum   module address (for &lt;LNCV_MOD_L&gt; and &lt;LNCV_MOD_H&gt;),
342     *                    same field is used for CV Value in WRITE to and READ_REPLY from Module
343     * @param cmdData     signals programming start/stop: LNCV_DATA_PRON/LNCV_DATA_PROFF
344     * @return LocoNet message for the requested instruction
345     * @throws IllegalArgumentException of command is not a valid LNCV Programming Format &lt;LNCV_CMD&gt; value
346     */
347    public static LocoNetMessage createLncvMessage(int opc,
348                                                   int source,
349                                                   int destination,
350                                                   int command,
351                                                   int articleNum,
352                                                   int cvNum,
353                                                   int moduleNum,
354                                                   int cmdData) throws IllegalArgumentException {
355
356        if (!isSupportedLncvCommand(command, opc, cmdData)) {
357            throw new IllegalArgumentException("Command is not a supported LNCV command"); // NOI18N
358        }
359        LocoNetMessage m = new LocoNetMessage(LNCV_LENGTH_ELEMENT_VALUE);
360
361        m.setOpCode(opc);
362        m.setElement(LNCV_LENGTH_ELEMENT_INDEX, LNCV_LENGTH_ELEMENT_VALUE);
363        m.setElement(LNCV_SRC_ELEMENT_INDEX, source);
364        m.setElement(LNCV_DST_L_ELEMENT_INDEX, (destination & 0xff));
365        m.setElement(LNCV_DST_H_ELEMENT_INDEX, (destination >> 8));
366        //log.debug("element {} = command = {}", LNCV_CMD_ELEMENT_INDEX, command);
367        m.setElement(LNCV_CMD_ELEMENT_INDEX, command);
368
369        int svx1 = 0x0;
370        svx1 = svx1 + (((articleNum & 0x80) == 0x80) ? LNCV_ART_L_ARTL7_CHECK_MASK : 0);
371        svx1 = svx1 + (((articleNum & 0x8000) == 0x8000) ? LNCV_ART_H_ARTH7_CHECK_MASK : 0);
372        svx1 = svx1 + (((cvNum & 0x80) == 0x80) ? LNCV_CVN_L_CVNL7_CHECK_MASK : 0);
373        svx1 = svx1 + (((cvNum & 0x8000) == 0x8000) ? LNCV_CVN_H_CVNH7_CHECK_MASK : 0);
374        svx1 = svx1 + (((moduleNum & 0x80) == 0x80) ? LNCV_MOD_L_MODL7_CHECK_MASK : 0);
375        svx1 = svx1 + (((moduleNum & 0x8000) == 0x8000) ? LNCV_MOD_H_MODH7_CHECK_MASK : 0);
376        //("Fetching hi bit {} of cmdData, value = {}", ((cmdData & 0x80) == 0x80), cmdData);
377        svx1 = svx1 + (((cmdData & 0x80) == 0x80) ? LNCV_CMDDATA_DAT7_CHECK_MASK : 0);
378        // bit 7 always 0
379        m.setElement(PXCT1_ELEMENT_INDEX, svx1);
380
381        m.setElement(LNCV_ART_L_ELEMENT_INDEX, (articleNum & 0x7f));
382        m.setElement(LNCV_ART_H_ELEMENT_INDEX, ((articleNum >> 8) & 0x7f));
383        m.setElement(LNCV_CVN_L_ELEMENT_INDEX, (cvNum & 0x7f));
384        m.setElement(LNCV_CVN_H_ELEMENT_INDEX, ((cvNum >> 8) & 0x7f));
385        m.setElement(LNCV_MOD_L_ELEMENT_INDEX, (moduleNum & 0x7f));
386        //log.debug("LNCV MOD_L = {}", m.getElement(LNCV_MOD_L_ELEMENT_INDEX));
387        m.setElement(LNCV_MOD_H_ELEMENT_INDEX, ((moduleNum >> 8) & 0x7f));
388        //log.debug("LNCV MOD_H = {}", m.getElement(LNCV_MOD_H_ELEMENT_INDEX));
389        m.setElement(LNCV_CMDDATA_ELEMENT_INDEX, (cmdData & 0x7f));
390
391        //log.debug("LocoNet Message ready, cmd = {}", m.getElement(LNCV_CMD_ELEMENT_INDEX));
392        return m;
393    }
394
395    /**
396     * Create LNCV message from {@link LncvCommand} enum plus specific parameter values.
397     *
398     * @param source source device (&lt;SRC&gt;)
399     * @param destination destination address (for &lt;DST_L&gt; and &lt;DST_H&gt;)
400     * @param command one of the composite LncvCommand's
401     * @param articleNum manufacturer's hardware class/article code as per specs
402     * @param cvNum 16-bit CV number (for &lt;LNCV_CVN_L&gt; and &lt;LNCV_CVN_H&gt;)
403     * @param moduleNum module address (for &lt;LNCV_MOD_L&gt; and &lt;LNCV_MOD_H&gt;),
404     *                    same field is used for CV Value in WRITE to and READ_REPLY from Module
405     * @return LocoNet message for the requested instruction
406     */
407    public static LocoNetMessage createLncvMessage(int source, int destination, LncvCommand command, int articleNum, int cvNum, int moduleNum) {
408        return createLncvMessage(command.getOpc(), source, destination, command.getCmd(), articleNum, cvNum, moduleNum, command.getCmdData());
409    }
410
411    public int getCmd() {
412        return cmd;
413    }
414
415    public int getCvNum() {
416        if ((cmd == LncvCommand.LNCV_READ.cmd) ||
417                (cmd == LncvCommand.LNCV_WRITE.cmd) ||
418                (cmd == LncvCommand.LNCV_READ_REPLY.cmd) ||
419                (cmd == LncvCommand.LNCV_READ_REPLY2.cmd)) {
420            return cvn;
421        }
422        return -1;
423    }
424
425    public int getCvValue() {
426        if ((cmd == LncvCommand.LNCV_READ_REPLY.cmd) ||
427                (cmd == LncvCommand.LNCV_READ_REPLY2.cmd) ||
428                (cmd == LncvCommand.LNCV_WRITE.cmd)) {
429            return mod;
430        }
431        return -1;
432    }
433
434    public int getLncvArticleNum() {
435        if ((cmd == LncvCommand.LNCV_READ.cmd) ||
436                (cmd == LncvCommand.LNCV_WRITE.cmd) ||
437                (cmd == LncvCommand.LNCV_READ_REPLY.cmd) ||
438                (cmd == LncvCommand.LNCV_READ_REPLY2.cmd) ||
439                (cmd == LncvCommand.LNCV_PROG_START.cmd && art != LNCV_ALL) ||
440                (cmd == LncvCommand.LNCV_PROG_END.cmd && art != LNCV_ALL)) {
441            return art;
442        }
443        return -1;
444    }
445
446    public int getLncvModuleNum() {
447        if (cmd == LncvCommand.LNCV_READ.cmd ||
448                (cmd == LncvCommand.LNCV_PROG_START.cmd && art != LNCV_ALL)||
449                (cmd == LncvCommand.LNCV_PROG_END.cmd && art != LNCV_ALL)) {
450            return mod;
451        }
452        return -1;
453    }
454    
455    /**
456     * Create LocoNet broadcast message to start LNCV programming.
457     *
458     * @param articleNum LNCV device type number used as filter to respond. Leave this out to 'broadcast' to
459     *                   all connected devices (which works for discovery purpose only)
460     * @return LocoNet message
461     */
462    public static LocoNetMessage createAllProgStartRequest(int articleNum) {
463        return createLncvMessage(
464                0x1,
465                0x5,
466                LncvCommand.LNCV_PROG_START,
467                (articleNum > -1 ? articleNum : LNCV_ALL),
468                0x0,
469                LNCV_ALL);
470    }
471
472    /**
473     * Create LocoNet broadcast message to end LNCV programming.
474     * (expect no reply from module)
475     *
476     * @param articleNum LNCV device type number used as filter to respond. Leave out to 'broadcast' to
477     *                   all connected devices (which works for discovery purpose only). Best to use same
478     *                   value as used while opening the session.
479     * @return LocoNet message
480     */
481    public static LocoNetMessage createAllProgEndRequest(int articleNum) {
482        return createLncvMessage(
483                0x1,
484                0x5,
485                LncvCommand.LNCV_PROG_END,
486                (articleNum > -1 ? articleNum : LNCV_ALL),
487                0x0,
488                LNCV_ALL); // replaces 0x1 from KD notes
489    }
490
491    /**
492     * Create LocoNet message for first query of a CV of this module.
493     *
494     * @param articleNum  address of the module
495     * @param moduleAddress  address of the module
496     * @return LocoNet message
497     */
498    public static LocoNetMessage createModProgStartRequest(int articleNum, int moduleAddress) {
499        return createLncvMessage(
500                0x1,
501                0x5,
502                LncvCommand.LNCV_PROG_START,
503                articleNum,
504                0x0,
505                moduleAddress); // effectively reads first CV0 = module address
506    }
507
508    /**
509     * Create LocoNet message to leave programming of this module.
510     * (expect no reply from module)
511     *
512     * @param articleNum  address of the module
513     * @param moduleAddress  address of the module
514     * @return LocoNet message
515     */
516    public static LocoNetMessage createModProgEndRequest(int articleNum, int moduleAddress) {
517        //log.debug("MODPROG_END {} message created", moduleAddress);
518        return createLncvMessage(
519                0x1,
520                0x5,
521                LncvCommand.LNCV_PROG_END,
522                articleNum,
523                0x0,
524                moduleAddress);
525    }
526
527    /**
528     * Create LocoNet message for a write to a CV of this object.
529     *
530     * @param articleNum  address of the module
531     * @param cvNum  CV number to query
532     * @param newValue new value to store in CV
533     * @return LocoNet message
534     */
535    public static LocoNetMessage createCvWriteRequest(int articleNum, int cvNum, int newValue) {
536        return createLncvMessage(
537                0x1,
538                0x5,
539                LncvCommand.LNCV_WRITE,
540                articleNum,
541                cvNum,
542                newValue);
543    }
544
545    /**
546     * Create LocoNet message for a query of a CV of this object.
547     *
548     * @param articleNum  address of the module
549     * @param cvNum  CV number to query
550     * @param moduleAddress  address of the module
551     * @return LocoNet message
552     */
553    public static LocoNetMessage createCvReadRequest(int articleNum, int moduleAddress, int cvNum) {
554        return createLncvMessage(
555                0x1,
556                0x5,
557                LncvCommand.LNCV_READ,
558                articleNum,
559                cvNum,
560                moduleAddress);
561    }
562
563    /* These 2 static methods are used to mock replies to requests from JMRI */
564
565    /**
566     * In Hexfile simulation mode, mock a ReadReply message back to the CS (when simulate replies is ON).
567     *
568     * @param m  the LocoNet message to respond to
569     * @return  LocoNet message containing the reply, or null if preceding
570     *          message isn't a query
571     */
572    public static LocoNetMessage createLncvReadReply(LocoNetMessage m) {
573        if (!isLnMessageASpecificLncvCommand(m, LncvCommand.LNCV_READ)) {
574            return null;
575        }
576        LocoNetMessage reply = new LocoNetMessage(m);
577        reply.setOpCode(LnConstants.OPC_PEER_XFER);
578        reply.setElement(LNCV_LENGTH_ELEMENT_INDEX, LNCV_LENGTH_ELEMENT_VALUE);
579
580        reply.setElement(LNCV_DST_L_ELEMENT_INDEX, (reply.getElement(LNCV_SRC_ELEMENT_INDEX) == LNCV_CS_SRC_VALUE ? 0x49 : reply.getElement(LNCV_SRC_ELEMENT_INDEX)));
581        reply.setElement(LNCV_DST_H_ELEMENT_INDEX, (reply.getElement(LNCV_SRC_ELEMENT_INDEX) == LNCV_CS_SRC_VALUE ? 0x4b : 0x00));
582
583        // set SRC after reading old value to determine DST above
584        reply.setElement(LNCV_SRC_ELEMENT_INDEX, LNCV_LNMODULE_VALUE);
585        reply.setElement(5, LNCV_CMD_READ_REPLY);
586        // HIBITS handled last
587        reply.setElement(7, reply.getElement(7));
588        reply.setElement(8, reply.getElement(8));
589        reply.setElement(9, reply.getElement(9));
590        reply.setElement(10, reply.getElement(10));
591        if (reply.getElement(9) != 0 || reply.getElement(10) != 0) { // if CV=0, keep cv value as is, it was passed in as the module address
592            reply.setElement(LNCV_MOD_L_ELEMENT_INDEX, 0x8); // random cv value_low
593            reply.setElement(LNCV_MOD_H_ELEMENT_INDEX, 0x1); // random cv value_hi
594            reply.setElement(PXCT1_ELEMENT_INDEX, reply.getElement(PXCT1_ELEMENT_INDEX)^0x60); // HIBITS recalculate (only elements 11-12 have changed = HIBITS bits 5 & 6)
595        }
596        reply.setElement(13, 0x0);
597
598        return reply;
599    }
600
601    /**
602     * In Hexfile simulation mode, mock a ProgStart reply message back to the CS.
603     *
604     * @param m the LocoNet message to respond to
605     * @return  LocoNet message containing the reply, or null if preceding
606     *          message isn't a query
607     */
608    public static LocoNetMessage createLncvProgStartReply(LocoNetMessage m) {
609        if (!isLnMessageASpecificLncvCommand(m, LncvCommand.LNCV_PROG_START)) {
610            return null;
611        }
612        LncvMessageContents lmc = new LncvMessageContents(m);
613        log.debug("request to article {}", lmc.getLncvArticleNum());
614        LocoNetMessage forward = new LocoNetMessage(m);
615        forward.setElement(LncvMessageContents.LNCV_CMDDATA_ELEMENT_INDEX, 0x00); // correct CMDDATA for ReadRequest (0x80 also observed)
616        forward.setElement(LncvMessageContents.PXCT1_ELEMENT_INDEX, m.getElement(PXCT1_ELEMENT_INDEX)^0x40); // together with this HIBIT
617        if (lmc.getLncvArticleNum() == LNCV_ALL) { // mock a certain device
618            log.debug("art ALL");
619            forward.setElement(LncvMessageContents.LNCV_ART_L_ELEMENT_INDEX, 0x29); // article number 5033
620            forward.setElement(LncvMessageContents.LNCV_ART_H_ELEMENT_INDEX, 0x13);
621            forward.setElement(LncvMessageContents.PXCT1_ELEMENT_INDEX, 0x01); // hibits to go with 5033
622        }
623        if (lmc.getLncvModuleNum() == LNCV_ALL) { // mock a certain address
624            log.debug("mod ALL");
625            forward.setElement(LncvMessageContents.LNCV_MOD_L_ELEMENT_INDEX, 0x3); // address value 3
626            forward.setElement(LncvMessageContents.LNCV_MOD_H_ELEMENT_INDEX, 0x0);
627        }
628        return LncvMessageContents.createLncvReadReply(forward);
629    }
630
631    /**
632     * Create LocoNet message to set aseries of Track-Control module display LEDs.
633     *
634     * @param moduleAddress  address of the module
635     * @param ledValue  CV number to query
636     * @param range2 true if intended for LED2 Command (leds 16-31), fasle for LED1 (0-15)
637     * @return LocoNet message
638     */
639    public static LocoNetMessage createDirectWriteRequest(int moduleAddress, int ledValue, boolean range2) {
640        return createLncvMessage(
641                LNCV_PC_SRC_VALUE,
642                0x5,
643                (range2 ? LncvCommand.LNCV_DIRECT_LED2 : LncvCommand.LNCV_DIRECT_LED1),
644                6900,
645                moduleAddress, // special: CV position [D3-D4] contains the module address
646                ledValue);
647    }
648
649    /**
650     * LNCV Commands mapped to unique sets of 3 parts in message. LNCV knows only 3 simple &lt;CMD&gt; values.
651     */
652    public enum LncvCommand { // full commands mapped to 3 values in message, LNCV knows only 3 simple CMD commands
653        LNCV_WRITE (LNCV_CMD_WRITE, LnConstants.OPC_IMM_PACKET, 0x00), // CMD=0x20, CmdData=0x0
654        // LNCV_WRITE_REPLY = LACK
655        LNCV_READ (LNCV_CMD_READ, LnConstants.OPC_IMM_PACKET, 0x00), // CMD=0x21, CmdData=0x0
656        LNCV_READ_REPLY (LNCV_CMD_READ_REPLY, LnConstants.OPC_PEER_XFER, 0x00), // CMD=0x1f, CmdData=0x0
657        LNCV_READ_REPLY2 (LNCV_CMD_READ_REPLY, LnConstants.OPC_PEER_XFER, 0x80), // CMD=0x1f, CmdData=0x0
658        LNCV_PROG_START (LNCV_CMD_READ, LnConstants.OPC_IMM_PACKET, LNCV_DATA_PRON_MASK), // CMD=0x21, CmdData=0x80
659        LNCV_PROG_END (LNCV_CMD_READ, LnConstants.OPC_PEER_XFER, LNCV_DATA_PROFF_MASK), // CMD=0x21, CmdData=0x40
660        LNCV_DIRECT_LED1 (LNCV_CMD_WRITE, LnConstants.OPC_IMM_PACKET, LNCV_DATA_LED1_MASK), // CMD=0x20, CmdData=0xff
661        LNCV_DIRECT_LED2 (LNCV_CMD_WRITE, LnConstants.OPC_IMM_PACKET, LNCV_DATA_LED2_MASK), // CMD=0x20, CmdData=0xfe
662        LNCV_DIRECT_REPLY (LNCV_CMD_READ_REPLY, LnConstants.OPC_PEER_XFER, LNCV_DATA_LED1_MASK); // CMD=0x1f, CmdData=0xff
663
664        private final int cmd;
665        private final int opc;
666        private final int cmddata;
667        
668        LncvCommand(int cmd, int opc, int cmddata) {
669            this.cmd = cmd;
670            this.opc = opc;
671            this.cmddata = cmddata;
672        }
673
674        int getCmd() {return cmd;}
675        int getOpc() {return opc;}
676        int getCmdData() {return cmddata;}
677        
678        public static int getCmd(LncvCommand mt) {
679            return mt.getCmd();
680        }
681
682        public Boolean matches(int matchCommand, int matchOpc, int matchData) {
683            //log.debug("CMD ENUM command {}={}? {}", matchCommand, cmd, (matchCommand == cmd));
684            //log.debug("CMD ENUM opc {}={}? {}", matchOpc, opc, (matchOpc == opc));
685            //log.debug("CMD ENUM commanddata {}={}? {}", matchData, cmddata, (matchData == cmddata));
686            return ((matchCommand == cmd) && (matchOpc == opc) && (matchData == cmddata));
687        }
688    }
689
690    // initialize logging
691    private final static Logger log = LoggerFactory.getLogger(LncvMessageContents.class);
692    
693}