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