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}