001package jmri.jmrix.dcc4pc;
002
003import java.util.Collection;
004import java.util.Collections;
005import java.util.Hashtable;
006import java.util.List;
007import java.util.Map;
008import java.util.concurrent.CopyOnWriteArrayList;
009import jmri.CollectingReporter;
010import jmri.DccLocoAddress;
011import jmri.IdTag;
012import jmri.RailCom;
013import jmri.Reporter;
014import jmri.Sensor;
015import jmri.implementation.AbstractRailComReporter;
016
017/**
018 * Extend jmri.implementation.AbstractRailComReporter for Dcc4Pc Reporters.
019 * Implementation for providing status of rail com decoders at this
020 * reporter location.
021 * <p>
022 * The reporter will decode the rail com packets and add the information to the
023 * rail com tag.
024 *
025 * @author Kevin Dickerson Copyright (C) 2012
026 */
027public class Dcc4PcReporter extends AbstractRailComReporter implements CollectingReporter {
028
029    public Dcc4PcReporter(String systemName, String userName) {  // a human-readable Reporter number must be specified!
030        super(systemName, userName);  // can't use prefix here, as still in construction
031    }
032
033    // The locos in this block, oldest first. Dcc4PcBlockLocos decides who is
034    // present, from channel 2 replies and clean channel 1 addresses, so that
035    // several locos in one block can be told apart.
036    // It's changed on the packet decode thread and read by the GUI (the Reporter
037    // table), so it's a copy-on-write list and readers always see a consistent
038    // snapshot.
039    private final List<IdTag> entryOrder = new CopyOnWriteArrayList<>();
040
041    /** @return the locos in this block, oldest first, as one consistent snapshot */
042    private IdTag[] entrySnapshot() {
043        return entryOrder.toArray(new IdTag[0]);
044    }
045
046    /**
047     * {@inheritDoc}
048     */
049    @Override
050    @SuppressWarnings("unchecked")
051    public Collection<Object> getCollection() {
052        return (Collection<Object>) (Collection<?>) Collections.unmodifiableCollection(entryOrder);
053    }
054
055    private static RailCom tagFor(int address) {
056        return (RailCom) jmri.InstanceManager.getDefault(jmri.RailComManager.class).provideIdTag("" + address);
057    }
058
059    /**
060     * A loco has been found in this block.
061     *
062     * @param address loco address
063     * @param isLong  true for a long (14-bit) address
064     */
065    void locoEntered(int address, boolean isLong) {
066        RailCom tag = tagFor(address);
067        tag.setDccAddress(new DccLocoAddress(address, isLong));
068        RailCom.Orientation o = orientation();
069        if (o != null) {
070            tag.setOrientation(o);
071        }
072        entryOrder.remove(tag);
073        entryOrder.add(tag);
074        notify(tag);
075    }
076
077    /**
078     * A loco already in this block has been heard again: update its "last
079     * seen" time. This doesn't make it the current report, so with several
080     * locos here the report doesn't flip between them.
081     *
082     * @param address loco address
083     */
084    void locoHeard(int address) {
085        RailCom tag = tagFor(address);
086        if (entryOrder.contains(tag)) {
087            tag.setWhereLastSeen(this);
088        }
089    }
090
091    /**
092     * A loco has left this block.
093     *
094     * @param address loco address
095     */
096    void locoExited(int address) {
097        RailCom tag = tagFor(address);
098        if (!entryOrder.remove(tag)) {
099            return;
100        }
101        if (getCurrentReport() == tag) {
102            updateCurrentReportAfterExit();
103        } else {
104            notifyCollectionUpdated();
105        }
106    }
107
108    /**
109     * Report the most recently entered loco still in this block, as
110     * {@link jmri.jmrix.openlcb.OlcbReporter} does.
111     */
112    private void updateCurrentReportAfterExit() {
113        IdTag[] tags = entrySnapshot();
114        if (tags.length == 0) {
115            notify(null);
116            return;
117        }
118        for (int i = tags.length - 1; i >= 0; i--) {
119            IdTag candidate = tags[i];
120            if (candidate.getWhereLastSeen() == this) {
121                notify(candidate);
122                return;
123            }
124        }
125        IdTag fallback = tags[tags.length - 1];
126        setReport(fallback);
127        setState(IdTag.SEEN);
128    }
129
130    /**
131     * {@inheritDoc}
132     * <p>
133     * Exits are reported by {@link #locoExited(int)}, so a loco seen elsewhere
134     * stays in the collection until then; only the current report moves on.
135     */
136    @Override
137    public void notifySeenElsewhere(IdTag tag, Reporter newReporter) {
138        if (!this.equals(newReporter) && getCurrentReport() == tag) {
139            IdTag[] tags = entrySnapshot();
140            for (int i = tags.length - 1; i >= 0; i--) {
141                IdTag candidate = tags[i];
142                if (candidate != tag && candidate.getWhereLastSeen() == this) {
143                    notify(candidate);
144                    return;
145                }
146            }
147        }
148    }
149
150    /**
151     * The report text is worked out when a loco is reported; if that loco is
152     * the current report and its data (such as speed) changes later, work it
153     * out again so the Reporter table shows the new value.
154     */
155    private void refreshReport(RailCom tag) {
156        if (getCurrentReport() == tag) {
157            setReport(tag);
158            firePropertyChange("currentReport", null, tag);
159        }
160    }
161
162    /** @return the orientation the input reports, or null if it reports none */
163    private RailCom.Orientation orientation() {
164        if (state == REPRESENTS_RAILCOM_EAST) {
165            return RailCom.Orientation.EAST;
166        } else if (state == REPRESENTS_RAILCOM_WEST) {
167            return RailCom.Orientation.WEST;
168        }
169        return null;
170    }
171
172    /**
173     * {@inheritDoc}
174     */
175    @Override
176    public boolean hasExitReports() {
177        return true;
178    }
179
180    // data members
181    transient RailComPacket[] rcPacket = new RailComPacket[3];
182
183    void setPacket(int[] arraytemp, int dcc_addr_type, int dcc_addr, int cvNumber, int speed, int packetTypeCmd) {
184        log.debug("{} dcc_addr {} {} {}", getDisplayName(), dcc_addr, cvNumber, speed);
185        RailComPacket rc = new RailComPacket(arraytemp, dcc_addr_type, dcc_addr, cvNumber, speed);
186        decodeRailComInfo(rc, packetTypeCmd);
187        rcPacket[2] = rcPacket[1];
188        rcPacket[1] = rcPacket[0];
189        rcPacket[0] = rc;
190        synchronized(lock) {
191            log.debug("Packets Seen {} in error {}", packetseen, packetsinerror);
192        }
193    }
194
195    static class RailComPacket {
196
197        transient final int[] rcPacket;
198        int dcc_addr_type;
199        int dccAddress;
200        int cvNumber;
201        int speed;
202
203        RailComPacket(int[] array, int dcc_addr_type, int dcc_addr, int cvNumber, int speed) {
204            rcPacket = array;
205            this.dcc_addr_type = dcc_addr_type;
206            this.dccAddress = dcc_addr;
207            this.cvNumber = cvNumber;
208            this.speed = speed;
209        }
210
211        int[] getPacket() {
212            return rcPacket;
213        }
214
215        int getAddressType() {
216            return dcc_addr_type;
217        }
218
219        int getDccAddress() {
220            return dccAddress;
221        }
222
223        int getCvNumber() {
224            return cvNumber;
225        }
226
227        int getSpeed() {
228            return speed;
229        }
230
231        String toHexString() {
232            StringBuilder buf = new StringBuilder();
233            buf.append("0x").append(Integer.toHexString(0xFF & rcPacket[0]));
234            for (int i = 1; i < rcPacket.length; i++) {
235                buf.append(", 0x").append(Integer.toHexString(0xFF & rcPacket[i]));
236            }
237            return buf.toString();
238        }
239    }
240
241    void duplicatePacket(int dup) {
242        RailComPacket temp;
243        switch (dup) {
244            case 0x00:
245                break; //re-use the rcPacket at the head.
246            case 0x02:
247                temp = rcPacket[1];  //move rcPacket one to the head
248                rcPacket[1] = rcPacket[0];
249                rcPacket[0] = temp;
250                break;
251            case 0x03:
252                temp = rcPacket[2]; //move rcPacket two to the head
253                rcPacket[2] = rcPacket[1];
254                rcPacket[1] = rcPacket[0];
255                rcPacket[0] = temp;
256                break;
257            default:
258                break;
259        }
260    }
261
262    int state = Sensor.UNKNOWN;
263    
264    static final int REPRESENTS_RAILCOM_EAST = 0x10;
265    static final int REPRESENTS_RAILCOM_WEST = 0x20;
266        
267    public void setRailComState(int ori) {
268        if (state == ori) {
269            return;
270        }
271        state = ori;
272        if (ori == Sensor.INACTIVE || ori == Sensor.UNKNOWN) {
273            //We reset everything as the associated sensor has gone inactive
274            synchronized (this) {
275                addr = 0;
276                address_part_1 = 0; // no ADR_HIGH yet; 0x100 would pass for short-address high byte 0
277                address_part_2 = -1;
278                addr_type = -1;
279                actual_speed = -1;
280                actual_load = -1;
281                actual_temperature = -1;
282                fuelLevel = -1;
283                waterLevel = -1;
284                location = -1;
285                routing_no = -1;
286            }
287            cvNumber = -1;
288            cvValues = new Hashtable<>();
289            entryOrder.clear();
290            setReport(null);
291        } else if (ori == REPRESENTS_RAILCOM_EAST || ori == REPRESENTS_RAILCOM_WEST) {
292            // the reader gives one orientation per input, so it applies to every loco here
293            RailCom.Orientation orientation = orientation();
294            for (IdTag t : entryOrder) {
295                ((RailCom) t).setOrientation(orientation);
296            }
297            if (super.getCurrentReport() instanceof RailCom) {
298                ((RailCom) super.getCurrentReport()).setOrientation(orientation);
299            }
300            firePropertyChange("currentReport", null, null);
301        }
302    }
303
304    public int getRailComState() {
305        return state;
306    }
307
308    public String getReport() {
309        if (super.getCurrentReport() != null && super.getCurrentReport() instanceof RailCom) {
310            return ((RailCom) super.getCurrentReport()).getTagID();
311        }
312        if ((getRailComState() < REPRESENTS_RAILCOM_EAST) || (rcPacket[0] == null) || rcPacket[0].getPacket() == null) {
313            return "";
314        }
315        return "";
316    }
317
318    //packet Length is a temp store used for decoding the railcom packet
319    int packetLength = 0;
320
321    void setPacketLength(int i) {
322        packetLength = i;
323    }
324
325    int getPacketLength() {
326        return packetLength;
327    }
328
329    int addr = 0;
330    int address_part_1 = 0; // 0x100 bit set once an ADR_HIGH has arrived
331    int address_part_2 = -1;
332    int addr_type = -1;
333    int actual_speed = -1;
334    int actual_load = -1;
335    int actual_temperature = -1;
336    int fuelLevel = -1;
337    int waterLevel = -1;
338    int location = -1;
339    int routing_no = -1;
340    int cvNumber = -1;
341    int cvvalue = -1;
342
343    int addressp1found = 0;
344
345    static int packetseen = 0;
346    static int packetsinerror = 0;
347    private static final Object lock = new Object();
348
349    Hashtable<Integer, Integer> cvValues = new Hashtable<>();
350
351    /**
352     * The first two bytes of a cutout's data are channel 1. Channel 2 is a
353     * separate time slot, so a bad byte in channel 1 (often two decoders'
354     * address broadcasts colliding) mustn't throw away channel 2: on
355     * 2026-10-13 that discarded POM answers that came with channel 1
356     * {@code 95 04}.
357     */
358    static final int CHANNEL_1_LENGTH = 2;
359
360    void decodeRailComInfo(RailComPacket rc, int packetTypeCmd) {
361
362        synchronized(lock) {
363            addressp1found++;
364            RailCom rcTag = null;
365
366            if (super.getCurrentReport() instanceof RailCom) {
367                rcTag = (RailCom) super.getCurrentReport();
368            }
369            // Channel 2 data (POM values, DYN) comes from the loco the DCC packet was sent
370            // to, which with several locos here needn't be the current report.
371            // Only that decoder replies on channel 2, so if it isn't one of the locos here,
372            // the data belongs to nobody here; never give it to whichever loco is current.
373            RailCom ch2Tag = null;
374            if (rc.getDccAddress() > 0) {
375                RailCom dest = tagFor(rc.getDccAddress());
376                if (entryOrder.contains(dest)) {
377                    ch2Tag = dest;
378                }
379            }
380            int[] packet = rc.getPacket();
381            char chbyte;
382            char type;
383
384            if (log.isDebugEnabled()) {
385                log.debug("{} packet type {}", getDisplayName(), packetTypeCmd);
386                log.debug("decodeRailComInfo {} {}", this.getDisplayName(), super.getCurrentReport());
387                StringBuilder buf = new StringBuilder();
388                for (int i = 0; i < packet.length; ++i) {
389                    buf.append(packet[i]);
390                }
391                log.debug("Rail Comm Packets {}", buf);
392
393            }
394            int i = 0;
395            while (i < packet.length) {
396                packetseen++;
397                chbyte = (char) packet[i];
398                chbyte = decode[chbyte];
399                if (chbyte == ERROR) {
400                    if (log.isDebugEnabled()) {
401                        log.debug("{} Error packet stage 1: {}", this.getDisplayName(), Integer.toHexString(packet[i]));
402                    }
403                    packetsinerror++;
404                    if (i < CHANNEL_1_LENGTH) {
405                        i = CHANNEL_1_LENGTH; // channel 2 is a separate slot: still decode it
406                        continue;
407                    }
408                    return;
409                }
410                i++;
411                if ((chbyte & ACK) == ACK) {
412                    // ACK, NACK or BUSY: a single symbol, and channel 2 may hold several
413                    continue;
414                }
415                if (packet.length <= i) {
416                    break;
417                }
418                type = chbyte;
419                chbyte = (char) packet[i];
420                chbyte = decode[chbyte];
421                if ((chbyte == ERROR) || ((chbyte & ACK) == ACK)) {
422                    if (log.isDebugEnabled()) {
423                        log.debug("{} Error packet stage 3 {}\n{}", this.getDisplayName(), Integer.toHexString(packet[i]), rc.toHexString());
424                    }
425                    i++;
426                    packetsinerror++;
427                    if (i <= CHANNEL_1_LENGTH) {
428                        i = CHANNEL_1_LENGTH; // channel 2 is a separate slot: still decode it
429                        continue;
430                    }
431                    return;
432                }
433
434                chbyte = (char) (((type & 0x03) << 6) | (chbyte & 0x3f));
435                type = (char) ((type >> 2) & 0x0F);
436                // RCN-217: EXT (3) and DYN (7) datagrams are three symbols long and
437                // IDs 8-11 six; the rest are two. Skipping the extra symbols stops
438                // them being read as the start of another datagram, such as a
439                // bogus ADR_HIGH from a DYN subindex.
440                int extraSymbols = 0;
441                if (type == 3 || type == 7) {
442                    extraSymbols = 1;
443                } else if (type >= 8 && type <= 11) {
444                    extraSymbols = 4;
445                }
446
447                switch (type) {
448                    case 0:
449                        log.debug("{} CV Value {}{}", this.getDisplayName(), (int) chbyte, ch2Tag);
450                        cvvalue = chbyte;
451                        if (ch2Tag != null && rc.getCvNumber() > 0) {
452                            // Only the reply to a POM packet for the CV being read or written
453                            // answers it: replies to earlier POM packets, still being repeated,
454                            // arrive too, and were taken as the answer (2026-10-13).
455                            if (ch2Tag.getExpectedCv() == rc.getCvNumber()) {
456                                ch2Tag.setCvValue(chbyte);
457                            } else {
458                                ch2Tag.setCV(rc.getCvNumber(), chbyte);
459                            }
460                        }
461                        break;
462                    case 4:
463                        if (log.isDebugEnabled()) {
464                            log.debug("{} Create/Get id tag for {}", this.getDisplayName(), rc.getDccAddress());
465                        }
466                        addr = rc.getDccAddress();
467                        addr_type = rc.getAddressType();
468                        break;
469                    case 1: // Address byte 1
470                        log.debug("Address Byte 1");
471                        // only 0 (short), 10xxxxxx (long) or 0110xxxx (consist) are addresses
472                        if (chbyte != 0 && (chbyte & 0xC0) != 0x80 && (chbyte & 0xF0) != 0x60) {
473                            log.debug("{} Ignoring invalid address byte 1 {}", this.getDisplayName(), Integer.toHexString(chbyte));
474                            break;
475                        }
476                        address_part_1 = (0x100 | chbyte);
477                        addressp1found = 0;
478                        break;
479                    case 2: //Address byte 2
480                        log.debug("{} Address part 2:", this.getDisplayName());
481                        address_part_2 = chbyte;
482                        if (packetTypeCmd == 0x03) {
483                            log.debug("Type three packet so shouldn't not pair part one with part two if it came from the previous packet");
484                            //As the last command was a type 3, an address part one packet can not be paired with this address part two packet.  Therefore will set it back to default
485                            //address_part_1 = 0x100;
486                            // break;
487                        }
488                        if (!((address_part_1 & 0x100) == 0x100)) {
489                            log.debug("{} Break at Address part 1, part one not complete", this.getDisplayName());
490                            break;
491                        }
492                        addr = combineAddress(address_part_1 & 0xFF, address_part_2);
493                        rcTag = decodeAddress();
494                        break;
495                    case 3: // EXT (RCN-217), location information; not decoded yet
496                        log.debug("{} EXT datagram", this.getDisplayName());
497                        break;
498                    case 5: //Routing number
499                        routing_no = chbyte;
500                        if (rcTag != null) {
501                            rcTag.setRoutingNo(routing_no);
502                            rcTag.setWhereLastSeen(this);
503                        }
504                        break;
505                    case 6: //Location
506                        location = chbyte;
507                        if (rcTag != null) {
508                            rcTag.setLocation(location);
509                            rcTag.setWhereLastSeen(this);
510                        }
511                        break;
512                    case 7: // DYN (RCN-217): 8-bit value, then a 6-bit subindex in the third symbol
513                        if (i + 1 >= packet.length || decode[packet[i + 1]] == ERROR
514                                || (decode[packet[i + 1]] & ACK) == ACK) {
515                            log.debug("{} DYN datagram without a valid subindex", this.getDisplayName());
516                            break;
517                        }
518                        int subindex = decode[packet[i + 1]] & 0x3F;
519                        log.debug("{} DYN subindex {} value {}", this.getDisplayName(), subindex, (int) chbyte);
520                        if (subindex == 0) { // actual speed 0-255
521                            actual_speed = chbyte;
522                        } else if (subindex == 1) { // actual speed 256-511
523                            actual_speed = chbyte + 256;
524                        } else {
525                            break; // other DYN values are not decoded yet
526                        }
527                        if (ch2Tag != null && ch2Tag.getActualSpeed() != actual_speed) {
528                            ch2Tag.setActualSpeed(actual_speed);
529                            refreshReport(ch2Tag);
530                        }
531                        break;
532                    case 8: // XPOM (RCN-217, IDs 8-11); not decoded yet
533                    case 9:
534                    case 10:
535                    case 11:
536                        log.debug("{} XPOM datagram", this.getDisplayName());
537                        break;
538                    case 15: //CV Address  Value
539                        log.debug("{} CV Address and value:", this.getDisplayName());
540                        i = i + 2;
541                        //len = 4;
542                        break;
543                    default:
544                        log.debug("unknown railcom type packet {}", type);
545                        break;
546                }
547                i += 1 + extraSymbols;
548
549            }
550        }
551    }
552
553    RailCom decodeAddress() {
554        RailCom rcTag;
555        log.debug("{} Create/Get id tag for {}", this.getDisplayName(), addr);
556        rcTag = (RailCom)jmri.InstanceManager.getDefault(jmri.RailComManager.class).provideIdTag("" + addr);
557
558        if ((fuelLevel != -1)) {
559            rcTag.setFuelLevel(fuelLevel);
560        }
561        if ((waterLevel != -1)) {
562            rcTag.setWaterLevel(waterLevel);
563        }
564        if ((routing_no != -1)) {
565            rcTag.setRoutingNo(routing_no);
566        }
567        if ((location != -1)) {
568            rcTag.setLocation(location);
569        }
570        if ((actual_temperature != -1)) {
571            rcTag.setActualTemperature(actual_temperature);
572        }
573        if ((actual_load != -1)) {
574            rcTag.setActualLoad(actual_load);
575        }
576        if ((actual_speed != -1)) {
577            rcTag.setActualSpeed(actual_speed);
578        }
579        for (Map.Entry<Integer, Integer> entry : cvValues.entrySet()) {
580            rcTag.setCV(entry.getKey(), entry.getValue());
581            if (cvvalue != -1) {
582                rcTag.setCvValue(cvvalue);
583            }
584        }
585
586        address_part_1 = 0;
587        address_part_2 = -1;
588        // No notify(rcTag) here: Dcc4PcBlockLocos decides which locos are present
589        // and calls locoEntered(), which also sets the orientation.
590        return rcTag;
591    }
592
593    /**
594     * Combine the ADR_HIGH and ADR_LOW payloads of a RailCom channel 1
595     * broadcast into a locomotive address (RCN-217 5.2.1).
596     *
597     * @param high ADR_HIGH payload: 0 for a short address, 10xxxxxx for the
598     *             top six bits of a long address, 0110xxxx for a consist
599     * @param low  ADR_LOW payload
600     * @return the address
601     */
602    static int combineAddress(int high, int low) {
603        if ((high & 0xC0) == 0x80) {
604            return ((high & 0x3F) << 8) | (low & 0xFF);
605        }
606        return low & 0x7F;
607    }
608
609    RailCom provideTag(int address, int addr_type) {
610        log.debug("provide Tag");
611        RailCom rcTag = (RailCom) jmri.InstanceManager.getDefault(jmri.RailComManager.class).provideIdTag("" + address);
612        notify(rcTag);
613        return rcTag;
614    }
615
616    public static final char ACK = 0x80;
617    public static final char ACK_1 = 0x81;
618    public static final char ACK_2 = 0x82;
619    public static final char ACK_3 = 0x83;
620    public static final char ACK_4 = 0x84;
621    public static final char ACK_5 = 0x85;
622    public static final char ACK_6 = 0x86;
623    public static final char ERROR = 0xFF;
624
625    private static final char[] decode = new char[]{
626        ERROR, ERROR, ERROR, ERROR, ERROR, ERROR, ERROR, ERROR,
627        ERROR, ERROR, ERROR, ERROR, ERROR, ERROR, ERROR, ACK_1,
628        ERROR, ERROR, ERROR, ERROR, ERROR, ERROR, ERROR, 0x33,
629        ERROR, ERROR, ERROR, 0x34, ERROR, 0x35, 0x36, ERROR,
630        ERROR, ERROR, ERROR, ERROR, ERROR, ERROR, ERROR, 0x3A,
631        ERROR, ERROR, ERROR, 0x3B, ERROR, 0x3C, 0x37, ERROR,
632        ERROR, ERROR, ERROR, 0x3F, ERROR, 0x3D, 0x38, ERROR,
633        ERROR, 0x3E, 0x39, ERROR, ACK_5, ERROR, ERROR, ERROR,
634        ERROR, ERROR, ERROR, ERROR, ERROR, ERROR, ERROR, 0x24,
635        ERROR, ERROR, ERROR, 0x23, ERROR, 0x22, 0x21, ERROR,
636        ERROR, ERROR, ERROR, 0x1F, ERROR, 0x1E, 0x20, ERROR,
637        ERROR, 0x1D, 0x1C, ERROR, 0x1B, ERROR, ERROR, ERROR,
638        ERROR, ERROR, ERROR, 0x19, ERROR, 0x18, 0x1A, ERROR,
639        ERROR, 0x17, 0x16, ERROR, 0x15, ERROR, ERROR, ERROR,
640        ERROR, 0x25, 0x14, ERROR, 0x13, ERROR, ERROR, ERROR,
641        0x32, ERROR, ERROR, ERROR, ERROR, ERROR, ERROR, ERROR,
642        ERROR, ERROR, ERROR, ERROR, ERROR, ERROR, ERROR, ACK_2,
643        ERROR, ERROR, ERROR, 0x0E, ERROR, 0x0D, 0x0C, ERROR,
644        ERROR, ERROR, ERROR, 0x0A, ERROR, 0x09, 0x0B, ERROR,
645        ERROR, 0x08, 0x07, ERROR, 0x06, ERROR, ERROR, ERROR,
646        ERROR, ERROR, ERROR, 0x04, ERROR, 0x03, 0x05, ERROR,
647        ERROR, 0x02, 0x01, ERROR, 0x00, ERROR, ERROR, ERROR,
648        ERROR, 0x0F, 0x10, ERROR, 0x11, ERROR, ERROR, ERROR,
649        0x12, ERROR, ERROR, ERROR, ERROR, ERROR, ERROR, ERROR,
650        ERROR, ERROR, ERROR, ACK_3, ERROR, 0x2B, 0x30, ERROR,
651        ERROR, 0x2A, 0x2F, ERROR, 0x31, ERROR, ERROR, ERROR,
652        ERROR, 0x29, 0x2E, ERROR, 0x2D, ERROR, ERROR, ERROR,
653        0x2C, ERROR, ERROR, ERROR, ERROR, ERROR, ERROR, ERROR,
654        ERROR, ACK_6, 0x28, ERROR, 0x27, ERROR, ERROR, ERROR,
655        0x26, ERROR, ERROR, ERROR, ERROR, ERROR, ERROR, ERROR,
656        ACK_4, ERROR, ERROR, ERROR, ERROR, ERROR, ERROR, ERROR,
657        ERROR, ERROR, ERROR, ERROR, ERROR, ERROR, ERROR, ERROR};
658
659    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(Dcc4PcReporter.class);
660
661}