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