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 * 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 return MAP.getOrDefault(msg.getElement(0),getDefaultOpc()).getName(); 365 } 366 367 /** 368 * Return a string OPC of a CBUS Message 369 * 370 * @param msg CbusMessage 371 * @return decoded CBUS OPC, eg. "RTON" or "ACON2", else Reserved string. 372 * Empty String for Extended Frames as no OPC concept. 373 */ 374 @Nonnull 375 public static final String decodeopc(AbstractMessage msg) { 376 if ((msg instanceof CanFrame) && !((CanFrame) msg).extendedOrRtr()) { 377 return decodeopcNonExtended(msg); 378 } 379 else { 380 return ""; 381 } 382 } 383 384 /** 385 * Test if CBUS OpCode is known to JMRI. 386 * Performs Extended / RTR Frame check. 387 * 388 * @param msg CanReply or CanMessage 389 * @return True if opcode is known 390 */ 391 public static final boolean isKnownOpc(AbstractMessage msg){ 392 return ( MAP.get(msg.getElement(0))!=null 393 && ( msg instanceof CanFrame) 394 && (!((CanFrame) msg).extendedOrRtr())); 395 } 396 397 /** 398 * Test if CBUS OpCode represents a CBUS event. 399 * <p> 400 * Defined in the CBUS Developer Manual as accessory commands. 401 * Excludes fast clock. 402 * <p> 403 * ACON, ACOF, AREQ, ARON, AROF, ASON, ASOF, ASRQ, ARSON, ARSOF, 404 * ACON1, ACOF1, ARON1, AROF1, ASON1, ASOF1, ARSON1, ARSOF1, 405 * ACON2, ACOF2, ARON2, AROF2, ASON2, ASOF2, ARSON2, ARSOF2 406 * 407 * @param opc CBUS op code 408 * @return True if opcode represents an event 409 */ 410 public static final boolean isEvent(int opc) { 411 return MAP.getOrDefault(opc,getDefaultOpc()).getFilters().contains(CbusFilterType.CFEVENT); 412 } 413 414 /** 415 * Test if CBUS opcode represents a JMRI event table event. 416 * Event codes excluding request codes + fastclock. 417 * <p> 418 * ACON, ACOF, ARON, AROF, ASON, ASOF, ARSON, ARSOF, 419 * ACON1, ACOF1, ARON1, AROF1, ASON1, ASOF1, ARSON1, ARSOF1, 420 * ACON2, ACOF2, ARON2, AROF2, ASON2, ASOF2, ARSON2, ARSOF2, 421 * ACON3, ACOF3, ARON3, AROF3, ASON3, ASOF3, ARSON3, ARSOF3, 422 * 423 * @param opc CBUS op code 424 * @return True if opcode represents an event 425 */ 426 public static final boolean isEventNotRequest(int opc) { 427 return (MAP.getOrDefault(opc,getDefaultOpc()).getFilters().contains(CbusFilterType.CFEVENT) 428 && !MAP.getOrDefault(opc,getDefaultOpc()).getFilters().contains(CbusFilterType.CFREQUEST)); 429 } 430 431 /** 432 * Test if CBUS opcode represents a DCC Command Station Message 433 * <p> 434 * TOF, TON, ESTOP, RTOF, RTON, RESTP, KLOC, QLOC, DKEEP, 435 * RLOC, QCON, ALOC, STMOD, PCON, KCON, DSPD, DFLG, DFNON, DFNOF, SSTAT, 436 * DFUN, GLOC, ERR, RDCC3, WCVO, WCVB, QCVS, PCVS, RDCC4, WCVS, VCVS, 437 * RDCC5, WCVOA, RDCC6, PLOC, STAT, RSTAT 438 * 439 * @param opc CBUS op code 440 * @return True if opcode represents a dcc command 441 */ 442 public static final boolean isDcc(int opc) { 443 return MAP.getOrDefault(opc,getDefaultOpc()).getFilters().contains(CbusFilterType.CFCS); 444 } 445 446 /** 447 * Test if CBUS opcode represents an on event. 448 * <p> 449 * ACON, ARON, ASON, ARSON 450 * ACON1, ARON1, ASON1, ARSON1 451 * ACON2, ARON2, ASON2, ARSON2 452 * ACON3, ARON3, ASON3, ARSON3 453 * 454 * @param opc CBUS op code 455 * @return True if opcode represents an on event 456 */ 457 public static final boolean isOnEvent(int opc) { 458 return MAP.getOrDefault(opc,getDefaultOpc()).getFilters().contains(CbusFilterType.CFON); 459 } 460 461 /** 462 * Test if CBUS opcode represents an event request. 463 * Excludes node data requests RQDAT + RQDDS. 464 * AREQ, ASRQ 465 * 466 * @param opc CBUS op code 467 * @return True if opcode represents a short event 468 */ 469 public static final boolean isEventRequest(int opc) { 470 return MAP.getOrDefault(opc,getDefaultOpc()).getFilters().contains(CbusFilterType.CFREQUEST); 471 } 472 473 /** 474 * Test if CBUS opcode represents a short event. 475 * <p> 476 * ASON, ASOF, ASRQ, ARSON, ARSOF 477 * ASON1, ASOF1, ARSON1, ARSOF1 478 * ASON2, ASOF2, ARSON2, ARSOF2 479 * ASON3, ASOF3, ARSON3, ARSOF3 480 * 481 * @param opc CBUS op code 482 * @return True if opcode represents a short event 483 */ 484 public static final boolean isShortEvent(int opc) { 485 return MAP.getOrDefault(opc,getDefaultOpc()).getFilters().contains(CbusFilterType.CFSHORT); 486 } 487 488 /** 489 * Get the filters for a CBUS OpCode. 490 * 491 * @param opc CBUS op code 492 * @return Filter EnumSet 493 */ 494 @Nonnull 495 public static final EnumSet<CbusFilterType> getOpcFilters(int opc){ 496 return MAP.getOrDefault(opc,getDefaultOpc()).getFilters(); 497 } 498 499 /** 500 * Get the Name of a CBUS OpCode. 501 * 502 * @param opc CBUS op code 503 * @return Name if known, else empty String. 504 */ 505 @Nonnull 506 public static final String getOpcName(int opc){ 507 if ( MAP.get(opc)!=null){ 508 return MAP.get(opc).getName(); 509 } 510 return ""; 511 } 512 513 /** 514 * Get the Minimum Priority for a CBUS OpCode. 515 * 516 * @param opc CBUS op code 517 * @return Minimum Priority 518 */ 519 public static final int getOpcMinPriority(int opc){ 520 return MAP.getOrDefault(opc,getDefaultOpc()).getMinPri(); 521 } 522 523 private static final Map<Integer, CbusOpc> MAP = createMainMap(); 524 525 private static Map<Integer, CbusOpc> createMainMap() { 526 Map<Integer, CbusOpc> result = new HashMap<>(150); // 134 as of April 2022 527 try { 528 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); 529 // disable DOCTYPE declaration & setXIncludeAware to reduce Sonar security warnings 530 factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); 531 factory.setXIncludeAware(false); 532 DocumentBuilder builder = factory.newDocumentBuilder(); 533 Document document = builder.parse(FileUtil.getFile("program:xml/cbus/CbusOpcData.xml")); 534 document.getDocumentElement().normalize(); 535 536 //Get all opcs 537 NodeList nList = document.getElementsByTagName("CbusOpc"); 538 for (int temp = 0; temp < nList.getLength(); temp++) { 539 Node node = nList.item(temp); 540 if (node.getNodeType() == Node.ELEMENT_NODE) { 541 Element eElement = (Element) node; 542 543 // split the format string at each comma 544 String[] fields = eElement.getAttribute("decode").split("~"); 545 StringBuilder fieldbuf = new StringBuilder(); 546 547 for (String field : fields) { 548 if (field.startsWith("OPC_")) { 549 field = Bundle.getMessage(field); 550 } 551 fieldbuf.append(field); 552 } 553 554 EnumSet<CbusFilterType> filterSet = EnumSet.noneOf(CbusFilterType.class); 555 String[] filters = eElement.getAttribute("filter").split(","); 556 for (String filter : filters) { 557 CbusFilterType tmp = CbusFilterType.valueOf(filter); 558 filterSet.add(tmp); 559 } 560 561 result.put(jmri.util.StringUtil.getByte(0,eElement.getAttribute("hex")), 562 new CbusOpc( 563 Integer.parseInt(eElement.getAttribute("minPri")), 564 eElement.getAttribute("name"), 565 fieldbuf.toString(), 566 filterSet 567 )); 568 } 569 } 570 } catch (ParserConfigurationException | SAXException | IOException ex) { 571 log.error("Error importing xml file", ex); 572 } 573 return Collections.unmodifiableMap(result); 574 } 575 576 /** 577 * Get a CBUS OpCode with default unknown values. 578 * 579 * @return Default OPC 580 */ 581 @Nonnull 582 private static CbusOpc getDefaultOpc(){ 583 return new CbusOpc( 584 3,Bundle.getMessage("OPC_RESERVED"),"", 585 EnumSet.of(CbusFilterType.CFMISC,CbusFilterType.CFUNKNOWN)); 586 } 587 588 private static class CbusOpc { 589 private final int _minPri; 590 private final String _name; 591 private final String _decodeText; 592 private final EnumSet<CbusFilterType> _filterMap; 593 594 private CbusOpc(int minPri, String name, String decode, EnumSet<CbusFilterType> filterMap){ 595 _minPri = minPri; 596 _name = name; 597 _decodeText = decode; 598 _filterMap = filterMap; 599 } 600 601 private int getMinPri(){ 602 return _minPri; 603 } 604 605 private String getName(){ 606 return _name; 607 } 608 609 private String getDecode(){ 610 return _decodeText; 611 } 612 613 private EnumSet<CbusFilterType> getFilters(){ 614 return EnumSet.copyOf(_filterMap); 615 } 616 } 617 618 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(CbusOpCodes.class); 619 620}