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}