001package jmri.jmrix.can.cbus; 002 003import java.io.IOException; 004import java.util.Collections; 005import java.util.EnumSet; 006import java.util.HashMap; 007import java.util.Map; 008import javax.annotation.Nonnull; 009import javax.xml.parsers.DocumentBuilder; 010import javax.xml.parsers.DocumentBuilderFactory; 011import javax.xml.parsers.ParserConfigurationException; 012import jmri.jmrix.AbstractMessage; 013import jmri.jmrix.can.CanFrame; 014import jmri.util.FileUtil; 015import org.w3c.dom.Document; 016import org.w3c.dom.Element; 017import org.w3c.dom.Node; 018import org.w3c.dom.NodeList; 019import org.xml.sax.SAXException; 020 021/** 022 * Methods to decode CBUS opcodes 023 * 024 * https://github.com/MERG-DEV/CBUSlib 025 * @author Andrew Crosland Copyright (C) 2009, 2021 026 * @author Steve Young (C) 2018 027 */ 028public class CbusOpCodes { 029 030 private CbusOpCodes() { 031 throw new IllegalStateException("Utility class"); 032 } 033 034 /** 035 * Return a string representation of a decoded CBUS Message. 036 * See CbusOpcData.xml for replacement codes. 037 * Used in CBUS Console Log. 038 * @param msg CbusMessage to be decoded Return String decoded message 039 * @return decoded CBUS message 040 */ 041 @Nonnull 042 public static final String fullDecode(AbstractMessage msg) { 043 StringBuilder buf = new StringBuilder(); 044 // split the format string at each comma 045 String[] fields = MAP.getOrDefault(msg.getElement(0),getDefaultOpc()).getDecode().split(","); 046 047 int idx = 1; 048 for (int i = 0; i < fields.length; i++) { 049 if (fields[i].startsWith("%")) { // replace with bytes from the message 050 int value = 0; 051 int bytes = Integer.parseInt(fields[i].substring(1, 2)); 052 for (; bytes > 0; bytes--) { 053 value = value * 256 + msg.getElement(idx++); 054 } 055 fields[i] = String.valueOf(value); 056 } 057 else if (fields[i].startsWith("^2")) { // replace with loco id from 2 bytes 058 fields[i] = locoFromBytes(msg.getElement(idx++), msg.getElement(idx++) ); 059 } 060 else if (fields[i].startsWith("^S")) { // replace with speed string from 1 byte 061 fields[i] = speedDirFromByte(msg.getElement(idx++) ); 062 } 063 else if (fields[i].startsWith("$4")) { // replace the 4 bytes with event / node name ( if possible ) 064 int nn = (256*msg.getElement(idx++))+(msg.getElement(idx++)); 065 int en = (256*msg.getElement(idx++))+(msg.getElement(idx++)); 066 fields[i] = new CbusNameService().getEventNodeString(nn,en); 067 } 068 else if (fields[i].startsWith("$2")) { // replace the 2 bytes with node name ( if possible ) 069 int nodenum = (256*msg.getElement(idx++))+(msg.getElement(idx++)); 070 fields[i] = "NN:" + nodenum + " " + new CbusNameService().getNodeName(nodenum); 071 } 072 073 // concatenat to the result 074 buf.append(fields[i]); 075 } 076 077 // special cases 078 switch (msg.getElement(0)) { 079 case CbusConstants.CBUS_ERR: // extra info for ERR opc 080 buf.append(getCbusErr(msg)); 081 break; 082 case CbusConstants.CBUS_CMDERR: // extra info for CMDERR opc 083 if ((msg.getElement(3) > 0 ) && (msg.getElement(3) < 13 )) { 084 buf.append(Bundle.getMessage("CMDERR"+msg.getElement(3))); 085 } 086 break; 087 case CbusConstants.CBUS_GLOC: // extra info GLOC OPC 088 appendGloc(msg,buf); 089 break; 090 case CbusConstants.CBUS_FCLK: 091 return CbusClockControl.dateFromCanFrame(msg); 092 default: 093 break; 094 } 095 return buf.toString(); 096 } 097 098 private static void appendGloc(AbstractMessage msg, StringBuilder buf) { 099 buf.append(" "); 100 if (( ( ( msg.getElement(3) ) & 1 ) == 1 ) // bit 0 is 1 101 && ( ( ( msg.getElement(3) >> 1 ) & 1 ) == 1 )) { // bit 1 is 1 102 buf.append(Bundle.getMessage("invalidFlags")); 103 } 104 else if ( ( ( msg.getElement(3) ) & 1 ) == 1 ){ // bit 0 is 1 105 buf.append(Bundle.getMessage("stealRequest")); 106 } 107 else if ( ( ( msg.getElement(3) >> 1 ) & 1 ) == 1 ){ // bit 1 is 1 108 buf.append(Bundle.getMessage("shareRequest")); 109 } 110 else { // bit 0 and bit 1 are 0 111 buf.append(Bundle.getMessage("standardRequest")); 112 } 113 } 114 115 /** 116 * Return CBUS ERR OPC String. 117 * @param msg CanMessage or CanReply containing the CBUSERR OPC 118 * @return Error String 119 */ 120 @Nonnull 121 public static final String getCbusErr(AbstractMessage msg){ 122 StringBuilder buf = new StringBuilder(); 123 // elements 1 & 2 depend on element 3 124 switch (msg.getElement(3)) { 125 case 1: 126 buf.append(Bundle.getMessage("ERR_LOCO_STACK_FULL")) 127 .append(locoFromBytes(msg.getElement(1),msg.getElement(2))); 128 break; 129 case 2: 130 buf.append(Bundle.getMessage("ERR_LOCO_ADDRESS_TAKEN", 131 locoFromBytes(msg.getElement(1),msg.getElement(2)))); 132 break; 133 case 3: 134 buf.append(Bundle.getMessage("ERR_SESSION_NOT_PRESENT",msg.getElement(1))); 135 break; 136 case 4: 137 buf.append(Bundle.getMessage("ERR_CONSIST_EMPTY")) 138 .append(msg.getElement(1)); 139 break; 140 case 5: 141 buf.append(Bundle.getMessage("ERR_LOCO_NOT_FOUND")) 142 .append(msg.getElement(1)); 143 break; 144 case 6: 145 buf.append(Bundle.getMessage("ERR_CAN_BUS_ERROR")); 146 break; 147 case 7: 148 buf.append(Bundle.getMessage("ERR_INVALID_REQUEST")) 149 .append(locoFromBytes(msg.getElement(1),msg.getElement(2))); 150 break; 151 case 8: 152 buf.append(Bundle.getMessage("ERR_SESSION_CANCELLED",msg.getElement(1))); 153 break; 154 default: 155 break; 156 } 157 return buf.toString(); 158 } 159 160 /** 161 * Return Loco Address String 162 * 163 * @param byteA 1st loco byte 164 * @param byteB 2nd loco byte 165 * @return Loco Address String 166 */ 167 @Nonnull 168 public static final String locoFromBytes(int byteA, int byteB ) { 169 return new jmri.DccLocoAddress(((byteA & 0x3f) * 256 + byteB ), 170 ((byteA & 0xc0) != 0)).toString(); 171 } 172 173 /** 174 * Get text string of speed / direction. 175 * @param byteA the Speed / Direction byte value. 176 * @return translated String. 177 */ 178 @Nonnull 179 public static final String speedDirFromByte(int byteA) { 180 StringBuilder sb = new StringBuilder(); 181 sb.append(" "); 182 sb.append(Bundle.getMessage("SpeedCol")); 183 sb.append(" "); 184 sb.append(getSpeedFromByte(byteA)); 185 sb.append(" "); 186 sb.append(getDirectionFromByte(byteA)); 187 sb.append(" "); 188 return sb.toString(); 189 } 190 191 /** 192 * Get loco speed from byte value. 193 * @param speed byte value 0-255 of speed containing direction flag. 194 * @return interpreted String, maybe with EStop localised text. 195 */ 196 public static String getSpeedFromByte( int speed ) { 197 int noDirectionSpeed = speed & ~(1 << 7); 198 switch (noDirectionSpeed){ 199 case 0: 200 return "0"; 201 case 1: 202 return "0 " + Bundle.getMessage("EStop"); 203 default: 204 return String.valueOf(noDirectionSpeed-1); 205 } 206 } 207 208 /** 209 * Get localised direction from speed byte. 210 * @param speed 0-255, 0-127 Reverse, else Forwards. 211 * @return localised Forward or Reverse String. 212 */ 213 public static String getDirectionFromByte( int speed ) { 214 return Bundle.getMessage( ( speed >> 7 ) == 1 ? "FWD" : "REV"); 215 } 216 217 /** 218 * Return a string representation of a decoded CBUS Message 219 * 220 * @param msg CbusMessage to be decoded 221 * @return decoded message after extended frame check 222 */ 223 @Nonnull 224 public static final String decode(AbstractMessage msg) { 225 if (msg instanceof CanFrame) { 226 if (!((CanFrame) msg).isExtended()) { 227 return fullDecode(msg); 228 } 229 else { 230 return decodeExtended((CanFrame)msg); 231 } 232 } 233 return ""; 234 } 235 236 /** 237 * Return a string representation of a decoded Extended CBUS Message 238 * 239 * @param msg Extended CBUS CAN Frame to be decoded 240 * @return decoded message after extended frame check 241 */ 242 @Nonnull 243 public static final String decodeExtended(CanFrame msg) { 244 StringBuilder sb = new StringBuilder(Bundle.getMessage("decodeBootloader")); 245 switch (msg.getHeader()) { 246 case 4: // outgoing Bootload Command are always 8 data 247 int newAddress; 248 int newChecksum; 249 if (msg.getNumDataElements() == 8) { 250 switch (msg.getElement(5)) { // data payload of bootloader control frames 251 case CbusConstants.CBUS_BOOT_NOP: // 0 252 sb.append(Bundle.getMessage("decodeCBUS_BOOT_NOP")); 253 break; 254 case CbusConstants.CBUS_BOOT_RESET: // 1 255 sb.append(Bundle.getMessage("decodeCBUS_BOOT_RESET")); 256 break; 257 case CbusConstants.CBUS_BOOT_INIT: // 2 258 newAddress = ( msg.getElement(2)*65536+msg.getElement(1)*256+msg.getElement(0) ); 259 sb.append(Bundle.getMessage("decodeCBUS_BOOT_INIT",newAddress)); 260 break; 261 case CbusConstants.CBUS_BOOT_CHECK: // 3 262 newChecksum = ( msg.getElement(7)*256+msg.getElement(6) ); 263 sb.append(Bundle.getMessage("decodeCBUS_BOOT_CHECK",newChecksum)); 264 break; 265 case CbusConstants.CBUS_BOOT_TEST: // 4 266 sb.append(Bundle.getMessage("decodeCBUS_BOOT_TEST")); 267 break; 268 case CbusConstants.CBUS_BOOT_DEVID: // 5 269 sb.append(Bundle.getMessage("decodeCBUS_BOOT_DEVID")); 270 break; 271 case CbusConstants.CBUS_BOOT_BOOTID: // 6 272 sb.append(Bundle.getMessage("decodeCBUS_BOOT_BOOTID")); 273 break; 274 case CbusConstants.CBUS_BOOT_ENABLES: // 7 275 sb.append(Bundle.getMessage("decodeCBUS_BOOT_ENABLES")); 276 break; 277 default: 278 break; 279 } 280 } 281 break; 282 case 5: // outgoing pure data frames are always 8 data 283 if (msg.getNumDataElements() == 8) { 284 sb.append( Bundle.getMessage("OPC_DA")).append(" :"); 285 msg.appendHexElements(sb); 286 } 287 break; 288 case 0x10000004: // incoming Bootload Reply with variable data 289 switch (msg.getNumDataElements()) { 290 case 1: // 1 data 291 switch (msg.getElement(0)) { // data payload of bootloader control frames 292 case CbusConstants.CBUS_EXT_BOOT_ERROR: // 0 293 sb.append(Bundle.getMessage("decodeCBUS_EXT_BOOT_ERROR")); 294 break; 295 case CbusConstants.CBUS_EXT_BOOT_OK: // 1 296 sb.append(Bundle.getMessage("decodeCBUS_EXT_BOOT_OK")); 297 break; 298 case CbusConstants.CBUS_EXT_BOOTC: // 2 299 sb.append(Bundle.getMessage("decodeCBUS_EXT_BOOTC")); 300 break; 301 case CbusConstants.CBUS_EXT_BOOT_OUT_OF_RANGE: // 3 302 sb.append(Bundle.getMessage("decodeCBUS_EXT_BOOT_OUT_OF_RANGE")); 303 break; 304 default: 305 break; 306 } 307 break; 308 case 5: // 5 data 309 switch (msg.getElement(0)) { // data payload of bootloader control frames 310 case CbusConstants.CBUS_EXT_BOOTID: // 6 311 sb.append(Bundle.getMessage("decodeCBUS_EXT_BOOTID")); 312 break; 313 default: 314 break; 315 } 316 break; 317 case 7: // 7 data 318 switch (msg.getElement(0)) { // data payload of bootloader control frames 319 case CbusConstants.CBUS_EXT_DEVID: // 5 320 sb.append(Bundle.getMessage("decodeCBUS_EXT_DEVID")); 321 break; 322 default: 323 break; 324 } 325 break; 326 default: // All other data - not used 327 break; 328 } 329 break; 330 case 0x10000005: // incoming Bootload Data reply are always 1 data 331 if (msg.getNumDataElements() == 1) { 332 switch (msg.getElement(0)) { // data payload of bootloader control frames 333 case CbusConstants.CBUS_EXT_BOOT_ERROR: // 0 334 sb.append(Bundle.getMessage("decodeCBUS_EXT_BOOT_DATA_ERROR")); 335 break; 336 case CbusConstants.CBUS_EXT_BOOT_OK: // 1 337 sb.append(Bundle.getMessage("decodeCBUS_EXT_BOOT_DATA_OK")); 338 break; 339 case CbusConstants.CBUS_EXT_BOOT_OUT_OF_RANGE: // 3 340 sb.append(Bundle.getMessage("decodeCBUS_EXT_BOOT_OUT_OF_RANGE")); 341 break; 342 default: 343 break; 344 } 345 } 346 break; 347 default: 348 break; 349 } 350 if (sb.toString().equals(Bundle.getMessage("decodeBootloader"))){ 351 return(Bundle.getMessage("decodeUnknownExtended")); 352 } 353 return sb.toString(); 354 } 355 356 /** 357 * Return a string representation of a decoded CBUS OPC 358 * 359 * @param msg CbusMessage to be decoded Return String decoded OPC 360 * @return decoded CBUS OPC, eg. "RTON" or "ACON2", else Reserved string. 361 */ 362 @Nonnull 363 public static final String decodeopcNonExtended(AbstractMessage msg) { 364 var mapString = MAP.get(msg.getElement(0)); 365 return ( mapString != null ? mapString.getName() : 366 Bundle.getMessage("OPC_RESERVED") + " " + 367 msg.toMonitorString().toUpperCase()); // uppercase to emphasise hex value 368 } 369 370 /** 371 * Return a string OPC of a CBUS Message 372 * 373 * @param msg CbusMessage 374 * @return decoded CBUS OPC, eg. "RTON" or "ACON2", else Reserved string. 375 * Empty String for Extended Frames as no OPC concept. 376 */ 377 @Nonnull 378 public static final String decodeopc(AbstractMessage msg) { 379 if ((msg instanceof CanFrame) && !((CanFrame) msg).extendedOrRtr()) { 380 return decodeopcNonExtended(msg); 381 } 382 else { 383 return ""; 384 } 385 } 386 387 /** 388 * Test if CBUS OpCode is known to JMRI. 389 * Performs Extended / RTR Frame check. 390 * 391 * @param msg CanReply or CanMessage 392 * @return True if opcode is known 393 */ 394 public static final boolean isKnownOpc(AbstractMessage msg){ 395 return ( MAP.get(msg.getElement(0))!=null 396 && ( msg instanceof CanFrame) 397 && (!((CanFrame) msg).extendedOrRtr())); 398 } 399 400 /** 401 * Test if CBUS OpCode represents a CBUS event. 402 * <p> 403 * Defined in the CBUS Developer Manual as accessory commands. 404 * Excludes fast clock. 405 * <p> 406 * ACON, ACOF, AREQ, ARON, AROF, ASON, ASOF, ASRQ, ARSON, ARSOF, 407 * ACON1, ACOF1, ARON1, AROF1, ASON1, ASOF1, ARSON1, ARSOF1, 408 * ACON2, ACOF2, ARON2, AROF2, ASON2, ASOF2, ARSON2, ARSOF2 409 * 410 * @param opc CBUS op code 411 * @return True if opcode represents an event 412 */ 413 public static final boolean isEvent(int opc) { 414 return MAP.getOrDefault(opc,getDefaultOpc()).getFilters().contains(CbusFilterType.CFEVENT); 415 } 416 417 /** 418 * Test if CBUS opcode represents a JMRI event table event. 419 * Event codes excluding request codes + fastclock. 420 * <p> 421 * ACON, ACOF, ARON, AROF, ASON, ASOF, ARSON, ARSOF, 422 * ACON1, ACOF1, ARON1, AROF1, ASON1, ASOF1, ARSON1, ARSOF1, 423 * ACON2, ACOF2, ARON2, AROF2, ASON2, ASOF2, ARSON2, ARSOF2, 424 * ACON3, ACOF3, ARON3, AROF3, ASON3, ASOF3, ARSON3, ARSOF3, 425 * 426 * @param opc CBUS op code 427 * @return True if opcode represents an event 428 */ 429 public static final boolean isEventNotRequest(int opc) { 430 return (MAP.getOrDefault(opc,getDefaultOpc()).getFilters().contains(CbusFilterType.CFEVENT) 431 && !MAP.getOrDefault(opc,getDefaultOpc()).getFilters().contains(CbusFilterType.CFREQUEST)); 432 } 433 434 /** 435 * Test if CBUS opcode represents a DCC Command Station Message 436 * <p> 437 * TOF, TON, ESTOP, RTOF, RTON, RESTP, KLOC, QLOC, DKEEP, 438 * RLOC, QCON, ALOC, STMOD, PCON, KCON, DSPD, DFLG, DFNON, DFNOF, SSTAT, 439 * DFUN, GLOC, ERR, RDCC3, WCVO, WCVB, QCVS, PCVS, RDCC4, WCVS, VCVS, 440 * RDCC5, WCVOA, RDCC6, PLOC, STAT, RSTAT 441 * 442 * @param opc CBUS op code 443 * @return True if opcode represents a dcc command 444 */ 445 public static final boolean isDcc(int opc) { 446 return MAP.getOrDefault(opc,getDefaultOpc()).getFilters().contains(CbusFilterType.CFCS); 447 } 448 449 /** 450 * Test if CBUS opcode represents an on event. 451 * <p> 452 * ACON, ARON, ASON, ARSON 453 * ACON1, ARON1, ASON1, ARSON1 454 * ACON2, ARON2, ASON2, ARSON2 455 * ACON3, ARON3, ASON3, ARSON3 456 * 457 * @param opc CBUS op code 458 * @return True if opcode represents an on event 459 */ 460 public static final boolean isOnEvent(int opc) { 461 return MAP.getOrDefault(opc,getDefaultOpc()).getFilters().contains(CbusFilterType.CFON); 462 } 463 464 /** 465 * Test if CBUS opcode represents an event request. 466 * Excludes node data requests RQDAT + RQDDS. 467 * AREQ, ASRQ 468 * 469 * @param opc CBUS op code 470 * @return True if opcode represents a short event 471 */ 472 public static final boolean isEventRequest(int opc) { 473 return MAP.getOrDefault(opc,getDefaultOpc()).getFilters().contains(CbusFilterType.CFREQUEST); 474 } 475 476 /** 477 * Test if CBUS opcode represents a short event. 478 * <p> 479 * ASON, ASOF, ASRQ, ARSON, ARSOF 480 * ASON1, ASOF1, ARSON1, ARSOF1 481 * ASON2, ASOF2, ARSON2, ARSOF2 482 * ASON3, ASOF3, ARSON3, ARSOF3 483 * 484 * @param opc CBUS op code 485 * @return True if opcode represents a short event 486 */ 487 public static final boolean isShortEvent(int opc) { 488 return MAP.getOrDefault(opc,getDefaultOpc()).getFilters().contains(CbusFilterType.CFSHORT); 489 } 490 491 /** 492 * Get the filters for a CBUS OpCode. 493 * 494 * @param opc CBUS op code 495 * @return Filter EnumSet 496 */ 497 @Nonnull 498 public static final EnumSet<CbusFilterType> getOpcFilters(int opc){ 499 return MAP.getOrDefault(opc,getDefaultOpc()).getFilters(); 500 } 501 502 /** 503 * Get the Name of a CBUS OpCode. 504 * 505 * @param opc CBUS op code 506 * @return Name if known, else empty String. 507 */ 508 @Nonnull 509 public static final String getOpcName(int opc){ 510 if ( MAP.get(opc)!=null){ 511 return MAP.get(opc).getName(); 512 } 513 return ""; 514 } 515 516 /** 517 * Get the Minimum Priority for a CBUS OpCode. 518 * 519 * @param opc CBUS op code 520 * @return Minimum Priority 521 */ 522 public static final int getOpcMinPriority(int opc){ 523 return MAP.getOrDefault(opc,getDefaultOpc()).getMinPri(); 524 } 525 526 private static final Map<Integer, CbusOpc> MAP = createMainMap(); 527 528 private static Map<Integer, CbusOpc> createMainMap() { 529 Map<Integer, CbusOpc> result = new HashMap<>(150); // 134 as of April 2022 530 try { 531 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); 532 // disable DOCTYPE declaration & setXIncludeAware to reduce Sonar security warnings 533 factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); 534 factory.setXIncludeAware(false); 535 DocumentBuilder builder = factory.newDocumentBuilder(); 536 Document document = builder.parse(FileUtil.getFile("program:xml/cbus/CbusOpcData.xml")); 537 document.getDocumentElement().normalize(); 538 539 //Get all opcs 540 NodeList nList = document.getElementsByTagName("CbusOpc"); 541 for (int temp = 0; temp < nList.getLength(); temp++) { 542 Node node = nList.item(temp); 543 if (node.getNodeType() == Node.ELEMENT_NODE) { 544 Element eElement = (Element) node; 545 546 // split the format string at each comma 547 String[] fields = eElement.getAttribute("decode").split("~"); 548 StringBuilder fieldbuf = new StringBuilder(); 549 550 for (String field : fields) { 551 if (field.startsWith("OPC_")) { 552 field = Bundle.getMessage(field); 553 } 554 fieldbuf.append(field); 555 } 556 557 EnumSet<CbusFilterType> filterSet = EnumSet.noneOf(CbusFilterType.class); 558 String[] filters = eElement.getAttribute("filter").split(","); 559 for (String filter : filters) { 560 CbusFilterType tmp = CbusFilterType.valueOf(filter); 561 filterSet.add(tmp); 562 } 563 564 result.put(jmri.util.StringUtil.getByte(0,eElement.getAttribute("hex")), 565 new CbusOpc( 566 Integer.parseInt(eElement.getAttribute("minPri")), 567 eElement.getAttribute("name"), 568 fieldbuf.toString(), 569 filterSet 570 )); 571 } 572 } 573 } catch (ParserConfigurationException | SAXException | IOException ex) { 574 log.error("Error importing xml file", ex); 575 } 576 return Collections.unmodifiableMap(result); 577 } 578 579 /** 580 * Get a CBUS OpCode with default unknown values. 581 * 582 * @return Default OPC 583 */ 584 @Nonnull 585 private static CbusOpc getDefaultOpc(){ 586 return new CbusOpc( 587 3,Bundle.getMessage("OPC_RESERVED"),"", 588 EnumSet.of(CbusFilterType.CFMISC,CbusFilterType.CFUNKNOWN)); 589 } 590 591 private static class CbusOpc { 592 private final int _minPri; 593 private final String _name; 594 private final String _decodeText; 595 private final EnumSet<CbusFilterType> _filterMap; 596 597 private CbusOpc(int minPri, String name, String decode, EnumSet<CbusFilterType> filterMap){ 598 _minPri = minPri; 599 _name = name; 600 _decodeText = decode; 601 _filterMap = filterMap; 602 } 603 604 private int getMinPri(){ 605 return _minPri; 606 } 607 608 private String getName(){ 609 return _name; 610 } 611 612 private String getDecode(){ 613 return _decodeText; 614 } 615 616 private EnumSet<CbusFilterType> getFilters(){ 617 return EnumSet.copyOf(_filterMap); 618 } 619 } 620 621 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(CbusOpCodes.class); 622 623}