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