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