001package jmri.jmrix.loconet;
002
003import java.io.Serializable;
004import java.util.Objects;
005import javax.annotation.Nonnull;
006
007import org.slf4j.Logger;
008import org.slf4j.LoggerFactory;
009import jmri.jmrix.AbstractMessage;
010import jmri.jmrix.loconet.messageinterp.LocoNetMessageInterpret;
011/**
012 * Represents a single command or response on the LocoNet.
013 * <p>
014 * Content is represented with ints to avoid the problems with sign-extension
015 * that bytes have, and because a Java char is actually a variable number of
016 * bytes in Unicode.
017 * <p>
018 * Note that this class does not manage the upper bit of the message. By
019 * convention, most LocoNet messages have the upper bit set on the first byte,
020 * and on no other byte; but not all of them do, and that must be managed
021 * elsewhere.
022 * <p>
023 * Note that many specific message types are created elsewhere. In general, if
024 * more than one tool will need to use a particular format, it's useful to
025 * refactor it to here.
026 * <hr>
027 * This file is part of JMRI.
028 * <p>
029 * JMRI is free software; you can redistribute it and/or modify it under
030 * the terms of version 2 of the GNU General Public License as published
031 * by the Free Software Foundation. See the "COPYING" file for a copy
032 * of this license.
033 * <p>
034 * JMRI is distributed in the hope that it will be useful, but WITHOUT
035 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
036 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
037 * for more details.
038 * <p>
039 * Some of the message formats used in this class are Copyright Digitrax, Inc.
040 * and used with permission as part of the JMRI project. That permission does
041 * not extend to uses in other software products. If you wish to use this code,
042 * algorithm or these message formats outside of JMRI, please contact Digitrax
043 * Inc for separate permission.
044 *
045 * @author Bob Jacobsen Copyright (C) 2001
046 * @author B. Milhaupt Copyright (C) 2018
047 * @see jmri.jmrix.nce.NceMessage
048 * @see jmri.jmrix.AbstractMessage
049 */
050public class LocoNetMessage extends AbstractMessage implements Serializable {
051    // Serializable, serialVersionUID used by jmrix.loconet.locormi, please do not remove
052    static final long serialVersionUID = -7904918731667071828L;
053
054    /**
055     * Create a LocoNetMessage object without providing any
056     * indication of its size or contents.
057     * <p>
058     * Because a LocoNet message requires at least a size, if
059     * not actual contents, this constructor always logs an error.
060     */
061    public LocoNetMessage() {
062        _nDataChars = 0;
063        _dataChars = new int[1];
064        log.error("LocoNetMessage does not allow a constructor with no argument"); // NOI18N
065    }
066
067    /**
068     * Create a new object, representing a specific-length message.
069     * <p>
070     * Logs an error if len is less than 2
071     *
072     * @param len Total bytes in message, including opcode and error-detection
073     *            byte.
074     */
075    public LocoNetMessage(int len) {
076        if (len < 2) {
077            _nDataChars = 0;
078            _dataChars = new int[1];
079            log.error("LocoNetMessage does not allow object creation if length is less than 2."); // NOI18N
080            return;
081        }
082        _nDataChars = len;
083        _dataChars = new int[len];
084    }
085
086    /**
087     * Create a LocoNetMessage from a String
088     * <p>
089     * Because it is difficult to create a complete LocoNet object using a string,
090     * this method of AbstractMessage is not supported.
091     * <p>
092     * This constructor always logs an error
093     * @param s an unused parameter
094     */
095    public LocoNetMessage(String s) {
096        _nDataChars = 0;
097        _dataChars = new int[1];
098        log.error("LocoNetMessage does not allow a constructor with a 'String' argument"); // NOI18N
099    }
100
101    /**
102     * Create a message with specified contents.
103     * <p>
104     * This method logs an error and returns if the contents are too short to
105     * represent a valid LocoNet message.
106     *
107     * @param contents The array of contents for the message. The error check
108     *                 word must be present, e.g. a 4-byte message must have
109     *                 four values in the array
110     */
111    public LocoNetMessage(int[] contents) {
112        if (contents.length < 2) {
113            _nDataChars = 0;
114            _dataChars = new int[1];
115            log.error("Cannot create a LocoNet message of length shorter than two."); // NOI18N
116        }
117        _nDataChars = contents.length;
118        _dataChars = new int[contents.length];
119        for (int i = 0; i < contents.length; i++) {
120            this.setElement(i, contents[i]);
121        }
122    }
123
124    /**
125     * Create a message with specified contents.  Each element is forced into an
126     * 8-bit value.
127     * <p>
128     * This method logs an error and returns if the message length is too short
129     * to represent a valid LocoNet message.
130     *
131     * @param contents The array of contents for the message. The error check
132     *                 word must be present, e.g. a 4-byte message must have
133     *                 four values in the array
134     */
135    public LocoNetMessage(byte[] contents) {
136        if (contents.length < 2) {
137            _nDataChars = 0;
138            _dataChars = new int[1];
139            log.error("Cannot create a LocoNet message of length shorter than two."); // NOI18N
140        }
141        _nDataChars = contents.length;
142        _dataChars = new int[contents.length];
143        for (int i = 0; i < contents.length; i++) {
144            _dataChars[i] = contents[i] & 0xFF;
145        }
146    }
147
148    public LocoNetMessage(LocoNetMessage original) {
149        Objects.requireNonNull(original,
150                "Unable to create message by copying a null message"); // NOI18N
151
152        _nDataChars = original.getNumDataElements();
153        _dataChars = new int[_nDataChars];
154
155        for (int i = 0; i < original.getNumDataElements(); i++) {
156            _dataChars[i] = original._dataChars[i];
157        }
158    }
159
160    public void setOpCode(int i) {
161        _dataChars[0] = i;
162    }
163
164    public int getOpCode() {
165        return _dataChars[0];
166    }
167
168    /**
169     * Get a String representation of the op code in hex.
170     *
171     * @return string containing a hexadecimal representation of the message OpCode
172     */
173    public String getOpCodeHex() {
174        return "0x" + Integer.toHexString(getOpCode()); // NOI18N
175    }
176
177    /**
178     * Get a specific byte from the message
179     * <p>
180     * Logs an error and aborts if the index is beyond the length of the message.
181     *
182     * @param n  the byte index within the message
183     * @return integer value of the byte at the index within the message
184     */
185    @Override
186    public int getElement(int n) {
187        if (n < 0 || n >= _dataChars.length) {
188            log.error("reference element {} in message of {} elements: {}", // NOI18N
189                    n, _dataChars.length, this.toString()); // NOI18N
190            return -1;
191        }
192        return _dataChars[n] & 0xFF;
193    }
194
195    /**
196     * set a specific byte at a specific index in the message
197     * <p>
198     * Logs an error and aborts if the index is beyond the length of the message.
199     *
200     * @param n  the byte index within the message
201     * @param v  the value to be set
202     */
203    @Override
204    public void setElement(int n, int v) {
205        if (n < 0 || n >= _dataChars.length) {
206            log.error("reference element {} in message of {} elements: {}", // NOI18N
207                    n, _dataChars.length, this.toString()); // NOI18N
208            return;
209        }
210        _dataChars[n] = v & 0xFF;
211    }
212
213    /**
214     * Get a String representation of the entire message in hex.
215     *
216     * @return a string representation containing a space-delimited set of hexadecimal
217     *      values.
218     */
219    @Override
220    public String toString() {
221        int val;
222        StringBuffer sb = new StringBuffer();
223        for (int i = 0; i < _nDataChars; i++) {
224            if (i > 0) {
225                sb.append(' ');
226            }
227
228            val = _dataChars[i] & 0xFF;
229            sb.append(hexChars[val >> 4]);
230            sb.append(hexChars[val & 0x0F]);
231        }
232        return sb.toString();
233    }
234
235    /**
236     * Set the checksum byte(s) of this message.
237     */
238    public void setParity() {
239        // check for the D3 special case
240        if ((getOpCode() == LnConstants.RE_OPC_PR3_MODE) && (getNumDataElements() > 6)) {
241            // sum the D3 header separately
242            int sum = 0xFF;
243            for (int i = 0; i < 5; i++) {
244                sum = sum ^ getElement(i);
245            }
246            setElement(5, sum);
247            // sum back half to 0xFF
248            sum = 0xFF;
249            for (int i = 6; i < getNumDataElements() - 1; i++) {
250                sum = sum ^ getElement(i);
251            }
252            setElement(getNumDataElements() - 1, sum);
253            return;
254        }
255
256        // normal case - just sum entire message
257        int len = getNumDataElements();
258        int chksum = 0xff;  /* the seed */
259
260        int loop;
261
262        for (loop = 0; loop < len - 1; loop++) {  // calculate contents for data part
263            chksum ^= getElement(loop);
264        }
265        setElement(len - 1, chksum);  // checksum is last element of message    }
266    }
267
268    /**
269     * Check whether the message has a valid checksum.
270     *
271     * @return true if checksum is correct, else false
272     */
273    public boolean checkParity() {
274        int len = getNumDataElements();
275        int chksum = 0xff;  /* the seed */
276
277        int loop;
278
279        // check for the D3 special case
280        if ((getOpCode() == LnConstants.RE_OPC_PR3_MODE) && (len > 6)) {
281            // sum the D3 header separately
282            int sum = 0xFF;
283            for (loop = 0; loop < 5; loop++) {
284                sum = sum ^ getElement(loop);
285            }
286            if (getElement(5) != sum) {
287                return false;
288            }
289            // sum back half to 0xFF
290            sum = 0xFF;
291            for (loop = 6; loop < len - 1; loop++) {
292                sum = sum ^ getElement(loop);
293            }
294            if (getElement(len - 1) != sum) {
295                return false;
296            }
297            return true;
298        }
299
300        // normal case - just sum entire message
301        for (loop = 0; loop < len - 1; loop++) {  // calculate contents for data part
302            chksum ^= getElement(loop);
303        }
304        return (chksum == getElement(len - 1));
305    }
306
307    // decode messages of a particular form
308    // create messages of a particular form
309    /**
310     * Get the 8 data bytes from an OPC_PEER_XFR message.
311     *
312     * @return int[8] data bytes
313     */
314    public int[] getPeerXfrData() {
315        if (getOpCode() != LnConstants.OPC_PEER_XFER) {
316            log.error("getPeerXfrData called with wrong opcode {}", // NOI18N
317                    getOpCode());
318        }
319        if (getElement(1) != 0x10) {
320            log.error("getPeerXfrData called with wrong secondary code {}", // NOI18N
321                    getElement(1));
322        }
323        if (getNumDataElements() != 16) {
324            log.error("getPeerXfrData called with wrong length {}",  // NOI18N
325                    getNumDataElements());
326            return new int[] {0};
327        }
328
329        int[] data = new int[]{0, 0, 0, 0, 0, 0, 0, 0};
330
331        int pxct1 = getElement(5);
332        int pxct2 = getElement(10);
333
334        // fill the 8 data items
335        data[0] = (getElement(6) & 0x7F) + ((pxct1 & 0x01) != 0 ? 0x80 : 0);
336        data[1] = (getElement(7) & 0x7F) + ((pxct1 & 0x02) != 0 ? 0x80 : 0);
337        data[2] = (getElement(8) & 0x7F) + ((pxct1 & 0x04) != 0 ? 0x80 : 0);
338        data[3] = (getElement(9) & 0x7F) + ((pxct1 & 0x08) != 0 ? 0x80 : 0);
339
340        data[4] = (getElement(11) & 0x7F) + ((pxct2 & 0x01) != 0 ? 0x80 : 0);
341        data[5] = (getElement(12) & 0x7F) + ((pxct2 & 0x02) != 0 ? 0x80 : 0);
342        data[6] = (getElement(13) & 0x7F) + ((pxct2 & 0x04) != 0 ? 0x80 : 0);
343        data[7] = (getElement(14) & 0x7F) + ((pxct2 & 0x08) != 0 ? 0x80 : 0);
344
345        return data;
346    }
347
348    /**
349     * Two messages are the same if their entire data content is the same. We
350     * ignore the error-check byte to ease comparisons before a message is
351     * transmitted.
352     *
353     * @return true if objects contain the same message contents
354     */
355    @Override
356    public boolean equals(Object o) {
357        if (o == null) {
358            return false;   // basic contract
359        }
360        if (!(o instanceof LocoNetMessage)) {
361            return false;
362        }
363        LocoNetMessage m = (LocoNetMessage) o;
364        if (m._nDataChars != this._nDataChars) {
365            return false;
366        }
367        for (int i = 0; i < _nDataChars - 1; i++) {
368            if ((m._dataChars[i] & 0xFF) != (this._dataChars[i] & 0xFF)) {
369                return false;
370            }
371        }
372        return true;
373    }
374
375    @Override
376    public int hashCode() {
377        int r = _nDataChars;
378        if (_nDataChars > 0) {
379            r += _dataChars[0];
380        }
381        if (_nDataChars > 1) {
382            r += _dataChars[1] * 128;
383        }
384        if (_nDataChars > 2) {
385            r += _dataChars[2] * 128 * 128;
386        }
387        return r;
388    }
389
390    /**
391     * Interprets a LocoNet message into a string describing the
392     * message.
393     * <p>
394     * Where appropriate, this method presents both the JMRI "System Name" and
395     * the JMRI "User Name" (where available) for messages which contain control 
396     * or status information for a Turnout, Sensor or Reporter.
397     * <p>
398     * Display of "User Name" information is acquired from the appropriate "manager",
399     * via a reference to an object with an assembled "System Name".  This method 
400     * assumes a system connection "prefix" of "L" when assembling that system name.
401     * The remainder of the assembled system name depends on the message contents - 
402     * message type determines which JMRI object type letter to add - "T" for turnouts,
403     * "S" for sensors, and "R" for transponding reporters.
404     * <p>
405     * If the appropriate manager already has an object for the system name being
406     * referenced, the method requests the associated user name.  If a user name is
407     * returned, then the method uses that user name as part of the message.  If 
408     * there is no associated JMRI object configured, or if the associated JMRI
409     * object does not have a user name assigned, then the method does not display 
410     * a user name.
411     * <p>
412     * The method is not appropriate when the user has multiple LocoNet connections
413     * or when the user has a single LocoNet connection but has changed the connection
414     * prefix to something other than the default of "L".
415     *
416     * @return a human readable representation of the message.
417     */
418    @Override
419    public String toMonitorString(){
420          return toMonitorString("L"); // NOI18N
421    }
422
423    /**
424     * Interprets a LocoNet message into a string describing the
425     * message when a specific connection prefix is known.
426     * <p>
427     * Where appropriate, this method presents both the JMRI "System Name" and
428     * the JMRI "User Name" (where available) for messages which contain control 
429     * or status information for a Turnout, Sensor or Reporter.
430     * <p>
431     * Display of "User Name" information is acquired from the appropriate "manager",
432     * via a reference to an object with an assembled "System Name".  This method 
433     * uses system connection "prefix" as specified in the "prefix" argument when 
434     * assembling that system name.  The remainder of the assembled system name 
435     * depends on the message contents.  Message type determines which JMRI 
436     * object type letter is added after the "prefix" - "T" for turnouts, * "S" 
437     * for sensors, and "R" for transponding reporters.  The item number 
438     * specified in the LocoNet message is appended to finish the system name. 
439     * <p>
440     * If the appropriate manager already has an object for the system name being
441     * referenced, the method requests the associated user name.  If a user name is
442     * returned, then the method uses that user name as part of the message.  If 
443     * there is no associated JMRI object configured, or if the associated JMRI
444     * object does not have a user name assigned, then the method does not display 
445     * a user name.
446     *
447     * @param prefix  the "System Name" prefix denoting the connection
448     * @return a human readable representation of the message.
449     */
450    public String toMonitorString(@Nonnull String prefix){
451          return LocoNetMessageInterpret.interpretMessage(this, 
452                  prefix+"T", prefix+"S", prefix+"R");
453    }
454
455    /**
456     * Return a newly created OPC_PEER_XFR message.
457     *
458     * @param src  Source address
459     * @param dst  Destination address
460     * @param d    int[8] for the data contents or null
461     * @param code The instruction code placed in the pcxt1 pcxt2 bytes
462     * @return The formatted message
463     */
464    static public LocoNetMessage makePeerXfr(int src, int dst, int[] d, int code) {
465        LocoNetMessage msg = new LocoNetMessage(16);
466        msg.setOpCode(LnConstants.OPC_PEER_XFER);
467        msg.setElement(1, 0x10);  // 2nd part of op code
468
469        // accumulate the pxct1,2 bytes
470        int pxct1 = 0;
471        int pxct2 = 0;
472
473        // install the "CODE" in pxct1, pxct2
474        pxct1 |= (code & 0x7) * 0x10;       // lower 3 bits
475        pxct2 |= ((code & 0x38) / 8) * 0x10; // next 4 bits
476
477        // store the addresses
478        msg.setElement(2, src & 0x7F); //src
479        msg.setElement(3, dst & 0x7F); //dstl
480        msg.setElement(4, highByte(dst) & 0x7F); //dsth
481
482        // store the data bytes
483        msg.setElement(6, d[0] & 0x7F);
484        if (highBit(d[0])) {
485            pxct1 |= 0x01;
486        }
487        msg.setElement(7, d[1] & 0x7F);
488        if (highBit(d[1])) {
489            pxct1 |= 0x02;
490        }
491        msg.setElement(8, d[2] & 0x7F);
492        if (highBit(d[2])) {
493            pxct1 |= 0x04;
494        }
495        msg.setElement(9, d[3] & 0x7F);
496        if (highBit(d[3])) {
497            pxct1 |= 0x08;
498        }
499
500        msg.setElement(11, d[4] & 0x7F);
501        if (highBit(d[4])) {
502            pxct2 |= 0x01;
503        }
504        msg.setElement(12, d[5] & 0x7F);
505        if (highBit(d[5])) {
506            pxct2 |= 0x02;
507        }
508        msg.setElement(13, d[6] & 0x7F);
509        if (highBit(d[6])) {
510            pxct2 |= 0x04;
511        }
512        msg.setElement(14, d[7] & 0x7F);
513        if (highBit(d[7])) {
514            pxct2 |= 0x08;
515        }
516
517        // store the pxct1,2 values
518        msg.setElement(5, pxct1);
519        msg.setElement(10, pxct2);
520
521        return msg;
522    }
523
524    /**
525     * Check if a high bit is set, usually used to store it in some other
526     * location (LocoNet does not allow the high bit to be set in data bytes).
527     *
528     * @param val  value to be checked
529     * @return True if the argument has the high bit set
530     */
531    static protected boolean highBit(int val) {
532        if ((val & (~0xFF)) != 0) {
533            log.error("highBit called with too large value: 0x{}", // NOI18N
534                    Integer.toHexString(val));
535        }
536        return (0 != (val & 0x80));
537    }
538
539    static protected int lowByte(int val) {
540        return val & 0xFF;
541    }
542
543    static protected int highByte(int val) {
544        if ((val & (~0xFFFF)) != 0) {
545            log.error("highByte called with too large value: {}", // NOI18N
546                    Integer.toHexString(val));
547        }
548        return (val & 0xFF00) / 256;
549    }
550
551    /**
552     * Extract sensor address from a sensor message.  Does not verify
553     * that the message is a sensor message.
554     *
555     * @return address (in range 0 to n-1)
556     */
557    public int sensorAddr() {
558        int sw1 = getElement(1);
559        int sw2 = getElement(2);
560        int as = sw2 & 0x20;  // should be a LocoNet constant?
561        int high = sw2 & 0x0F;
562        int low = sw1 & 0x7F;
563        return high * 256 + low * 2 + (as != 0 ? 1 : 0);
564    }
565
566    /**
567     * If this is an OPC_INPUT_REP, get the 0-n address, else -1
568     *
569     * @return address (in range 0 to n-1)
570     */
571    public int inputRepAddr() {
572        if (getOpCode() == LnConstants.OPC_INPUT_REP) {
573            return sensorAddr();
574        } else {
575            return -1;
576        }
577    }
578
579    /**
580     * Get turnout address. Does not check to see that the message is
581     * a turnout message.
582     *
583     * @return address (in range 1 to n )
584     */
585    public int turnoutAddr() {
586        int a1 = getElement(1);
587        int a2 = getElement(2);
588        return (((a2 & 0x0f) * 128) + (a1 & 0x7f)) + 1;
589    }
590
591    // Hex char array for toString conversion
592    static char[] hexChars = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
593
594    // initialize logging
595    private final static Logger log = LoggerFactory.getLogger(LocoNetMessage.class);
596
597}