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}