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    private 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        if (original.getNumDataElements() >= 0)
159            System.arraycopy(original._dataChars, 0, _dataChars, 0, original.getNumDataElements());
160    }
161
162    public void setOpCode(int i) {
163        _dataChars[0] = i;
164    }
165
166    public int getOpCode() {
167        return _dataChars[0];
168    }
169
170    /**
171     * Get a String representation of the op code in hex.
172     *
173     * @return string containing a hexadecimal representation of the message OpCode
174     */
175    public String getOpCodeHex() {
176        return "0x" + Integer.toHexString(getOpCode()); // NOI18N
177    }
178
179    /**
180     * Get a specific byte from the message
181     * <p>
182     * Logs an error and aborts if the index is beyond the length of the message.
183     *
184     * @param n  the byte index within the message
185     * @return integer value of the byte at the index within the message
186     */
187    @Override
188    public int getElement(int n) {
189        if (n < 0 || n >= _dataChars.length) {
190            log.error("reference element {} in message of {} elements: {}", // NOI18N
191                    n, _dataChars.length, this); // NOI18N
192            return -1;
193        }
194        return _dataChars[n] & 0xFF;
195    }
196
197    /**
198     * Set a specific byte at a specific index in the message
199     * <p>
200     * Logs an error and aborts if the index is beyond the length of the message.
201     *
202     * @param n  the byte index within the message
203     * @param v  the value to be set
204     */
205    @Override
206    public void setElement(int n, int v) {
207        if (n < 0 || n >= _dataChars.length) {
208            log.error("reference element {} in message of {} elements: {}", // NOI18N
209                    n, _dataChars.length, this); // NOI18N
210            return;
211        }
212        _dataChars[n] = v & 0xFF;
213    }
214
215    /**
216     * Get a String representation of the entire message in hex.
217     *
218     * @return a string representation containing a space-delimited set of hexadecimal
219     *      values.
220     */
221    @Override
222    public String toString() {
223        int val;
224        StringBuilder sb = new StringBuilder();
225        for (int i = 0; i < _nDataChars; i++) {
226            if (i > 0) {
227                sb.append(' ');
228            }
229
230            val = _dataChars[i] & 0xFF;
231            sb.append(hexChars[val >> 4]);
232            sb.append(hexChars[val & 0x0F]);
233        }
234        return sb.toString();
235    }
236
237    /**
238     * Set the checksum byte(s) of this message.
239     */
240    public void setParity() {
241        // check for the D3 special case
242        if ((getOpCode() == LnConstants.RE_OPC_PR3_MODE) && (getNumDataElements() > 6)) {
243            // sum the D3 header separately
244            int sum = 0xFF;
245            for (int i = 0; i < 5; i++) {
246                sum = sum ^ getElement(i);
247            }
248            setElement(5, sum);
249            // sum back half to 0xFF
250            sum = 0xFF;
251            for (int i = 6; i < getNumDataElements() - 1; i++) {
252                sum = sum ^ getElement(i);
253            }
254            setElement(getNumDataElements() - 1, sum);
255            return;
256        }
257
258        // normal case - just sum entire message
259        int len = getNumDataElements();
260        int chksum = 0xff;  /* the seed */
261
262        int loop;
263
264        for (loop = 0; loop < len - 1; loop++) {  // calculate contents for data part
265            chksum ^= getElement(loop);
266        }
267        setElement(len - 1, chksum);  // checksum is last element of message
268    }
269
270    /**
271     * Check whether the message has a valid checksum.
272     *
273     * @return true if checksum is correct, else false
274     */
275    public boolean checkParity() {
276        int len = getNumDataElements();
277        int chksum = 0xff;  /* the seed */
278
279        int loop;
280
281        // check for the D3 special case
282        if ((getOpCode() == LnConstants.RE_OPC_PR3_MODE) && (len > 6)) {
283            // sum the D3 header separately
284            int sum = 0xFF;
285            for (loop = 0; loop < 5; loop++) {
286                sum = sum ^ getElement(loop);
287            }
288            if (getElement(5) != sum) {
289                return false;
290            }
291            // sum back half to 0xFF
292            sum = 0xFF;
293            for (loop = 6; loop < len - 1; loop++) {
294                sum = sum ^ getElement(loop);
295            }
296            return getElement(len - 1) == sum;
297        }
298
299        // normal case - just sum entire message
300        for (loop = 0; loop < len - 1; loop++) {  // calculate contents for data part
301            chksum ^= getElement(loop);
302        }
303        return (chksum == getElement(len - 1));
304    }
305
306    // decode messages of a particular form
307    // create messages of a particular form
308    /**
309     * Get the 8 data bytes from an OPC_PEER_XFR message.
310     *
311     * @return int[8] data bytes
312     */
313    public int[] getPeerXfrData() {
314        if (getOpCode() != LnConstants.OPC_PEER_XFER) {
315            log.error("getPeerXfrData called with wrong opcode {}", // NOI18N
316                    getOpCode());
317        }
318        if (getElement(1) != 0x10) {
319            log.error("getPeerXfrData called with wrong secondary code {}", // NOI18N
320                    getElement(1));
321        }
322        if (getNumDataElements() != 16) {
323            log.error("getPeerXfrData called with wrong length {}",  // NOI18N
324                    getNumDataElements());
325            return new int[] {0};
326        }
327
328        int[] data = new int[] {0, 0, 0, 0, 0, 0, 0, 0};
329
330        int pxct1 = getElement(5);
331        int pxct2 = getElement(10);
332
333        // fill the 8 data items
334        data[0] = (getElement(6) & 0x7F) + ((pxct1 & 0x01) != 0 ? 0x80 : 0);
335        data[1] = (getElement(7) & 0x7F) + ((pxct1 & 0x02) != 0 ? 0x80 : 0);
336        data[2] = (getElement(8) & 0x7F) + ((pxct1 & 0x04) != 0 ? 0x80 : 0);
337        data[3] = (getElement(9) & 0x7F) + ((pxct1 & 0x08) != 0 ? 0x80 : 0);
338
339        data[4] = (getElement(11) & 0x7F) + ((pxct2 & 0x01) != 0 ? 0x80 : 0);
340        data[5] = (getElement(12) & 0x7F) + ((pxct2 & 0x02) != 0 ? 0x80 : 0);
341        data[6] = (getElement(13) & 0x7F) + ((pxct2 & 0x04) != 0 ? 0x80 : 0);
342        data[7] = (getElement(14) & 0x7F) + ((pxct2 & 0x08) != 0 ? 0x80 : 0);
343
344        return data;
345    }
346
347    /**
348     * Two messages are the same if their entire data content is the same. We
349     * ignore the error-check byte to ease comparisons before a message is
350     * transmitted.
351     *
352     * @return true if objects contain the same message contents
353     */
354    @Override
355    public boolean equals(Object o) {
356        if (o == null) {
357            return false;   // basic contract
358        }
359        if (!(o instanceof LocoNetMessage)) {
360            return false;
361        }
362        LocoNetMessage m = (LocoNetMessage) o;
363        if (m._nDataChars != this._nDataChars) {
364            return false;
365        }
366        for (int i = 0; i < _nDataChars - 1; i++) {
367            if ((m._dataChars[i] & 0xFF) != (this._dataChars[i] & 0xFF)) {
368                return false;
369            }
370        }
371        return true;
372    }
373
374    @Override
375    public int hashCode() {
376        int r = _nDataChars;
377        if (_nDataChars > 0) {
378            r += _dataChars[0];
379        }
380        if (_nDataChars > 1) {
381            r += _dataChars[1] * 128;
382        }
383        if (_nDataChars > 2) {
384            r += _dataChars[2] * 128 * 128;
385        }
386        return r;
387    }
388
389    /**
390     * Interprets a LocoNet message into a string describing the
391     * message.
392     * <p>
393     * Where appropriate, this method presents both the JMRI "System Name" and
394     * the JMRI "User Name" (where available) for messages which contain control
395     * or status information for a Turnout, Sensor or Reporter.
396     * <p>
397     * Display of "User Name" information is acquired from the appropriate "manager",
398     * via a reference to an object with an assembled "System Name".  This method
399     * assumes a system connection "prefix" of "L" when assembling that system name.
400     * The remainder of the assembled system name depends on the message contents -
401     * message type determines which JMRI object type letter to add - "T" for turnouts,
402     * "S" for sensors, and "R" for transponding reporters.
403     * <p>
404     * If the appropriate manager already has an object for the system name being
405     * referenced, the method requests the associated user name.  If a user name is
406     * returned, then the method uses that user name as part of the message.  If
407     * there is no associated JMRI object configured, or if the associated JMRI
408     * object does not have a user name assigned, then the method does not display
409     * a user name.
410     * <p>
411     * The method is not appropriate when the user has multiple LocoNet connections
412     * or when the user has a single LocoNet connection but has changed the connection
413     * prefix to something other than the default of "L".
414     *
415     * @return a human readable representation of the message.
416     */
417    @Override
418    public String toMonitorString(){
419          return toMonitorString("L"); // NOI18N
420    }
421
422    /**
423     * Interprets a LocoNet message into a string describing the
424     * message when a specific connection prefix is known.
425     * <p>
426     * Where appropriate, this method presents both the JMRI "System Name" and
427     * the JMRI "User Name" (where available) for messages which contain control
428     * or status information for a Turnout, Sensor or Reporter.
429     * <p>
430     * Display of "User Name" information is acquired from the appropriate "manager",
431     * via a reference to an object with an assembled "System Name".  This method
432     * uses system connection "prefix" as specified in the "prefix" argument when
433     * assembling that system name.  The remainder of the assembled system name
434     * depends on the message contents.  Message type determines which JMRI
435     * object type letter is added after the "prefix" - "T" for turnouts, * "S"
436     * for sensors, and "R" for transponding reporters.  The item number
437     * specified in the LocoNet message is appended to finish the system name.
438     * <p>
439     * If the appropriate manager already has an object for the system name being
440     * referenced, the method requests the associated user name.  If a user name is
441     * returned, then the method uses that user name as part of the message.  If
442     * there is no associated JMRI object configured, or if the associated JMRI
443     * object does not have a user name assigned, then the method does not display
444     * a user name.
445     *
446     * @param prefix  the "System Name" prefix denoting the connection
447     * @return a human readable representation of the message.
448     */
449    public String toMonitorString(@Nonnull String prefix){
450          return LocoNetMessageInterpret.interpretMessage(this,
451                  prefix+"T", prefix+"S", prefix+"R");
452    }
453
454    /**
455     * Return a newly created OPC_PEER_XFR message.
456     *
457     * @param src  Source address
458     * @param dst  Destination address
459     * @param d    int[8] for the data contents or null
460     * @param code The instruction code placed in the pcxt1 pcxt2 bytes
461     * @return The formatted message
462     */
463    static public LocoNetMessage makePeerXfr(int src, int dst, int[] d, int code) {
464        log.debug("makePeerXfr for src={}, dst={} subAddr={} data[]={} code={}", src, dst, d[4], d, 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_l
479        msg.setElement(3, dst & 0x7F); // dst_l
480        msg.setElement(4, highByte(dst) & 0x7F); // dst_h
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    /**
592     * Two messages are the same if their masked data content is the same.
593     * <br>
594     * We ignore the error-check byte to ease comparisons before a message is
595     * transmitted.
596     *
597     * @param o the LocoNet message to be compared against this object's message
598     * @param masks an array of bytes to use to mask the corresponding bytes of 
599     * the messages.
600     * @return true if objects contain the same message contents
601     */
602    public boolean equals(LocoNetMessage o, int[] masks) {
603        if (o == null) {
604            return false;   // basic contract
605        }
606        LocoNetMessage m = new LocoNetMessage(o);
607        if (m._nDataChars != this._nDataChars) {
608            return false;
609        }
610        if (m._nDataChars != masks.length) {
611            return false;
612        }
613        for (int i = 0; i < _nDataChars - 1; i++) {
614            if ((m._dataChars[i] & masks[i]) != (this._dataChars[i] & masks[i])) {
615                return false;
616            }
617        }
618        return true;
619    }
620
621    // Hex char array for toString conversion
622    static char[] hexChars = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
623
624    // initialize logging
625    private final static Logger log = LoggerFactory.getLogger(LocoNetMessage.class);
626
627}