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}