001package jmri.jmrix.dcc4pc;
002
003import java.util.ArrayList;
004import java.util.Arrays;
005import java.util.Enumeration;
006import java.util.HashMap;
007import java.util.List;
008import java.util.concurrent.ConcurrentHashMap;
009import javax.annotation.Nonnull;
010import jmri.JmriException;
011import jmri.Sensor;
012
013/**
014 * Implement SensorManager for Dcc4Pc systems. The Manager handles all the state
015 * changes.
016 * <p>
017 * System names are "DSnn:yy", where D is the user configurable system prefix,
018 * nn is the board id and yy is the port on that board.
019 *
020 * @author Kevin Dickerson Copyright (C) 2009
021 */
022public class Dcc4PcSensorManager extends jmri.managers.AbstractSensorManager
023        implements Dcc4PcListener {
024
025    public Dcc4PcSensorManager(Dcc4PcTrafficController tc, Dcc4PcSystemConnectionMemo memo) {
026        super(memo);
027        this.tc = tc;
028        this.reportManager = (Dcc4PcReporterManager) memo.get(jmri.ReporterManager.class);
029        jmri.InstanceManager.store(Dcc4PcSensorManager.this, Dcc4PcSensorManager.class);
030        this.boardManager = new Dcc4PcBoardManager(tc, this);
031        // Finally, create and register a shutdown task to ensure clean exit
032        this.pollShutDownTask = this::stopPolling;
033        jmri.InstanceManager.getDefault(jmri.ShutDownManager.class).register(pollShutDownTask);
034        tc.setReconnectHandler(this::interfaceReconnected);
035        startPolling();
036    }
037
038    /**
039     * The interface's port has been reopened after a failure (its USB cable
040     * was unplugged and plugged back in). The interface has restarted at 8
041     * kbps but the boards may not have, so set the bus up as at startup, then
042     * bring each board back to 62.5 kbps and resynchronise it.
043     */
044    void interfaceReconnected() {
045        boardManager.rediscover();
046        for (ActiveBoard board : activeBoards.values()) {
047            if (!board.doNotPoll()) {
048                board.restartAfterReconnect();
049            }
050        }
051    }
052
053    Dcc4PcReporterManager reportManager;
054    Runnable pollShutDownTask;
055    Dcc4PcBoardManager boardManager;
056
057    Dcc4PcTrafficController tc;
058
059    @Override
060    public Dcc4PcSensor getSensor(@Nonnull String name) {
061        return (Dcc4PcSensor) super.getSensor(name);
062
063    }
064
065    /**
066     * {@inheritDoc}
067     */
068    @Override
069    @Nonnull
070    public Dcc4PcSystemConnectionMemo getMemo() {
071        return (Dcc4PcSystemConnectionMemo) memo;
072    }
073
074    /**
075     * {@inheritDoc}
076     */
077    @Override
078    @Nonnull
079    protected Sensor createNewSensor(@Nonnull String systemName, String userName) throws IllegalArgumentException {
080        Sensor s = new Dcc4PcSensor(systemName, userName);
081        s.setUserName(userName);
082        extractBoardID(systemName);
083        return s;
084    }
085
086    /**
087     * This extracts the board id out from the system name.
088     * @param systemName including system prefix and type letter.
089     */
090    void extractBoardID(String systemName) {
091        if (systemName.contains(":")) {
092            int indexOfSplit = systemName.indexOf(":");
093            systemName = systemName.substring(0, indexOfSplit);
094            indexOfSplit = getSystemPrefix().length() + 1; // +1 includes the typeletter which is a char
095            systemName = systemName.substring(indexOfSplit);
096            int boardNo;
097            try {
098                boardNo = Integer.parseInt(systemName);
099            } catch (NumberFormatException ex) {
100                log.error("Unable to find the board address from system name {}", systemName);
101                return;
102            }
103            addBoard(boardNo);
104        }
105    }
106
107    @Override
108    public boolean allowMultipleAdditions(@Nonnull String systemName) {
109        return true;
110    }
111
112    // we want the system name to be in the format of board:input
113    @Override
114    @Nonnull
115    public String createSystemName(@Nonnull String curAddress, @Nonnull String prefix) throws JmriException {
116        String iName;
117        if (curAddress.contains(":")) {
118            int board = 0;
119            int channel;
120            // Address format passed is in the form of board:channel or T:turnout address
121            int seperator = curAddress.indexOf(":");
122            try {
123                board = Integer.parseInt(curAddress.substring(0, seperator));
124            } catch (NumberFormatException ex) {
125                throw new JmriException("Unable to convert "+curAddress+" into the cab and channel format of nn:xx");
126            }
127
128            try {
129                channel = Integer.parseInt(curAddress.substring(seperator + 1));
130                if ((channel > 16) || (channel < 1)) {
131                    throw new JmriException("In Address "+curAddress+" Channel number should be in the range of 1 to 16");
132                }
133            } catch (NumberFormatException ex) {
134                throw new JmriException("Unable to convert "+curAddress+" into the cab and channel format of nn:xx");
135            }
136            iName = curAddress;
137            addBoard(board);
138        } else {
139            throw new JmriException("Unable to convert "+curAddress+" into the cab and channel format of nn:xx");
140        }
141        return prefix + typeLetter() + iName;
142    }
143
144    public void notifyReply(Dcc4PcReply m) {
145        // is this a list of sensors?
146    }
147
148    public void notifyMessage(Dcc4PcMessage m) {
149        // messages are ignored
150    }
151
152    Thread pollThread;
153    boolean stopPolling = true;
154
155    protected void stopPolling() {
156        synchronized (this) {
157            stopPolling = true;
158            notifyAll(); // don't sit out the rest of a poll wait
159        }
160        if (pollThread != null) {
161            // we want to wait for the polling thread to finish what it is currently working on.
162            try {
163                pollThread.join(5000);
164            } catch (InterruptedException e) {
165                // Don't need to worry
166            }
167        }
168    }
169
170    protected final void startPolling() {
171        if (stopPolling && pollThread != null) {
172            pollThread = null;
173        }
174        stopPolling = false;
175
176        if (pollThread == null) {
177            pollThread = new Thread(this::pollManager, "DCC4PC Sensor Poll");
178            pollThread.start();
179        }
180    }
181
182    void addBoard(int newBoard) {
183        boardManager.addBoard(newBoard);
184    }
185
186    @Override
187    public void reply(Dcc4PcReply r) {
188        if (log.isDebugEnabled()) {
189            log.debug("Reply details sm: {}", r.toHexString());
190        }
191
192        if (r.getMessageType() == Dcc4PcMessage.INFO) {
193            // the check that follows a speed change
194            ActiveBoard board = activeBoards.get(r.getBoard());
195            if (board != null) {
196                board.speedCheckDone(!r.isError() && r.getNumDataElements() > 0);
197            }
198            return;
199        }
200        if (r.getMessageType() == Dcc4PcMessage.CHILDRESET) {
201            ActiveBoard board = activeBoards.get(r.getBoard());
202            if (board != null) {
203                board.resetReplied(!r.isError());
204            }
205            return;
206        }
207        if (r.getNumDataElements() == 0 && r.getElement(0) == 0x00) {
208            //Simple acknowledgement reply, no further action required
209            return;
210        }
211        if (r.getBoard() == -1) {
212            log.debug("Message is not for a detection board so ignore");
213            return;
214        }
215        if (r.isError()) {
216            log.debug("Reply is in error {}", r.toHexString());
217            synchronized (this) {
218                awaitingReply = false;
219                this.notify();
220            }
221            if (r.getMessageType() == Dcc4PcMessage.CHILDPOLL) {
222                // the reader has moved on without us, so the stream is broken
223                ActiveBoard board = activeBoards.get(r.getBoard());
224                if (board != null) {
225                    board.requestResync("Get Data failed");
226                }
227            }
228        } else if (!r.isUnsolicited()) {
229            synchronized (this) {
230                awaitingReply = false;
231                this.notify();
232            }
233            if (log.isDebugEnabled()) {
234                log.debug("Get Data inputs {}", r.toHexString());
235            }
236            ActiveBoard board = activeBoards.get(r.getBoard());
237            if (board == null) {
238                log.error("Do not know who this board message is for");
239                return;
240            }
241            // the raw encoding is a stateful stream, so replies are decoded one at a time, in order
242            packetProcessor.execute(() -> board.processRawReply(r));
243        }
244    }
245
246    private final java.util.concurrent.ExecutorService packetProcessor =
247            java.util.concurrent.Executors.newSingleThreadExecutor(runnable -> {
248                Thread t = new Thread(runnable, "Dcc4PCSensor Process Packet");
249                t.setDaemon(true);
250                return t;
251            });
252
253    public static String decodeInputState(int state) {
254        String rtr;
255        switch (state) {
256            case Sensor.INACTIVE:
257                rtr = "UnOccupied";
258                break;
259            case Sensor.ACTIVE:
260                rtr = "Occupied No RailCom Data";
261                break;
262            case Dcc4PcSensor.ORIENTA:
263                rtr = "Occupied RailCom Orientation A";
264                break;
265            case Dcc4PcSensor.ORIENTB:
266                rtr = "Occupied RailCom Orientation B";
267                break;
268            default:
269                rtr = "Unknown";
270        }
271        return rtr;
272    }
273
274    public static final int NO_ADDRESS = 0x00;
275    public static final int SHORT_ADDRESS = 0x02;
276    public static final int LONG_ADDRESS = 0x04;
277    public static final int CONSIST_ADDRESS = 0x08;
278
279    private final int shortCycleInterval = 1; // Time to wait between sending out poll messages
280    private final int pollTimeout = 3500;    // in case of lost response; longer than the Get Data timeout
281    private boolean awaitingReply = false;
282
283    void pollManager() {
284        for (ActiveBoard board : activeBoards.values()) {
285            if (!board.doNotPoll() && board.speedChecked) {
286                board.requestResync("polling started");
287            }
288        }
289        while (!stopPolling) {
290            if (activeBoards.isEmpty()) {
291                //If we have no boards to poll then wait a second.
292                try {
293                    Thread.sleep(1000);
294                } catch (java.lang.InterruptedException e) {
295                }
296            } else {
297                for (int boardAddress : activeBoards.keySet()) {
298                    ActiveBoard board = activeBoards.get(boardAddress);
299                    if (!board.doNotPoll() && !board.isSynced()) {
300                        // Get Data is useless until a Reset Encoding State has been acknowledged
301                        board.checkSpeedTimeout();
302                        board.resendResetIfNeeded();
303                        try {
304                            Thread.sleep(50);
305                        } catch (InterruptedException e) {
306                            Thread.currentThread().interrupt();
307                        }
308                    } else if (!board.doNotPoll()) {
309                        board.retrySpeedIfDue();
310                        if (!board.isSynced()) {
311                            continue; // a speed change has just started
312                        }
313                        log.debug("Poll board {}", boardAddress);
314                        Dcc4PcMessage m = Dcc4PcMessage.pollBoard(boardAddress);
315                        log.debug("queueing poll request for board {}", boardAddress);
316                        tc.sendDcc4PcMessage(m, this);
317                        synchronized (this) {
318                            awaitingReply = true;
319                            try {
320                                wait(pollTimeout);
321                            } catch (InterruptedException e) {
322                                Thread.currentThread().interrupt(); // retain if needed later
323                            }
324                        }
325                        int delay = shortCycleInterval;
326                        synchronized (this) {
327                            if (awaitingReply && !stopPolling) { // stopPolling() wakes us early on purpose
328                                log.warn("timeout awaiting poll response for board {}", boardAddress);
329                                delay = pollTimeout;
330                            }
331                            try {
332                                wait(delay);
333                            } catch (InterruptedException e) {
334                                Thread.currentThread().interrupt(); // retain if needed later
335                            } finally {
336                                /*awaitingDelay = false;*/
337                            }
338                        }
339                    } else {
340                        log.debug("Board set to Do Not Poll {}", boardAddress);
341                    }
342                    synchronized (this) {
343                        if (stopPolling) {
344                            log.debug("Polling stopped {}", stopPolling);
345                            Thread.currentThread().interrupt();
346                            return;
347                        }
348                    }
349                }
350            }
351        }
352    }
353
354    ConcurrentHashMap<Integer, ActiveBoard> activeBoards = new ConcurrentHashMap<>(5);
355
356    /**
357     * Generate all the sensor and reporter details based upon
358     * the reply message from the board.
359     *
360     * @param r Reply that we build the information from
361     */
362    protected void createSensorsFromReply(Dcc4PcReply r) {
363        int boardAddress = r.getBoard();
364        log.debug("createSensorsFromReply: Get enabled inputs {}", r.toHexString());
365
366        String sensorPrefix = getSystemPrefix() + typeLetter() + boardAddress + ":";
367        String reporterPrefix = getSystemPrefix() + "R" + boardAddress + ":";
368
369        int x = 1;
370        List<Integer> enabled = new ArrayList<>();
371        for (int i = 0; i < r.getNumDataElements(); i++) {
372
373            for (int j = 0; j < 8; j++) {
374                Dcc4PcSensor s = (Dcc4PcSensor) createNewSensor(sensorPrefix + (j + x), null);
375                register(s);
376                s.setInput(j + x);
377                activeBoards.get(boardAddress).addSensor(j + x, s);
378                // one bit per input, the first input in bit 0
379                if (((r.getElement(i) >> j) & 0x01) == 0x01) {
380                    s.setEnabled(true);
381                    enabled.add(j + x);
382                }
383                Dcc4PcReporter report = (Dcc4PcReporter) reportManager.createNewReporter(reporterPrefix + (j + x), null);
384                activeBoards.get(boardAddress).addReporter(j + x, report);
385
386            }
387            x = x + 8;
388        }
389        log.info("Board {} has inputs {} enabled", boardAddress, enabled);
390        activeBoards.get(boardAddress).setEnabledInputs(enabled);
391        activeBoards.get(boardAddress).setDoNotPoll(false);
392        log.debug("     created {} sensors", x);
393    }
394
395    @Override
396    public void handleTimeout(Dcc4PcMessage m) {
397        log.debug("timeout received to our last message {}", m);
398        ActiveBoard board = activeBoards.get(m.getBoard());
399        if (board != null) {
400            if (m.getMessageType() == Dcc4PcMessage.INFO) {
401                board.speedCheckDone(false);
402            } else if (m.getMessageType() == Dcc4PcMessage.CHILDRESET) {
403                board.resetReplied(false);
404            } else if (m.getMessageType() == Dcc4PcMessage.CHILDPOLL) {
405                // the reply may have been lost part way, so the stream can't be trusted
406                board.requestResync("Get Data timed out");
407            }
408        }
409        if (!stopPolling) {
410            synchronized (this) {
411                awaitingReply = false;
412                this.notify();
413            }
414        }
415    }
416
417    @Override
418    public void message(Dcc4PcMessage m) {
419
420    }
421
422    public void changeBoardAddress(int oldAddress, int newAddress) {
423        // Block polling on this board
424        ActiveBoard board = activeBoards.get(oldAddress);
425        board.setDoNotPoll(true);
426        Dcc4PcMessage m = new jmri.jmrix.dcc4pc.Dcc4PcMessage(new byte[]{(byte) 0x0b, (byte) oldAddress, (byte) 0x03, (byte) newAddress});
427        tc.sendDcc4PcMessage(m, null);
428        // Need to stop polling otherwise we get a concurrent modification exception
429        stopPolling();
430        activeBoards.remove(oldAddress);
431        activeBoards.put(newAddress, board);
432        board.setAddress(newAddress);
433        startPolling();
434        String sensorPrefix = getSystemPrefix() + typeLetter() + newAddress + ":";
435        String reporterPrefix = getSystemPrefix() + "R" + newAddress + ":";
436        // We create a new set of sensors and reporters, but leave the old ones.
437        for (int i = 1; i < board.getNumEnabledSensors() + 1; i++) {
438            board.getSensorAtIndex(i).setEnabled(false);
439            int input = board.getSensorAtIndex(i).getInput();
440            Dcc4PcSensor s = (Dcc4PcSensor) createNewSensor(sensorPrefix + (input), null);
441            register(s);
442            s.setInput(input);
443            s.setEnabled(true);
444            board.addSensor(input, s);
445            Dcc4PcReporter report = (Dcc4PcReporter) reportManager.createNewReporter(reporterPrefix + (input), null);
446            board.addReporter(input, report);
447        }
448        // Need to update the sensors used.
449        board.setDoNotPoll(false);
450    }
451
452    class ActiveBoard {
453
454        ActiveBoard(int address, String version, int inputs, int encoding) {
455            this.version = version;
456            this.inputs = inputs;
457            this.encoding = encoding;
458            this.address = address;
459        }
460
461        void setAddress(int address) {
462            this.address = address;
463        }
464
465        int inputs;
466        int encoding;
467        String version;
468        String description;
469        int address;
470
471        int failedRequests = 0;
472
473        void addFailedRequests() {
474            failedRequests++;
475        }
476
477        void clearFailedRequests() {
478            failedRequests = 0;
479        }
480
481        boolean doNotPoll = true;
482
483        void setDoNotPoll(boolean poll) {
484            doNotPoll = poll;
485            if (!poll) {
486                if (speedChecked) {
487                    requestResync("polling enabled");
488                } else {
489                    startSpeedChange();
490                }
491            }
492        }
493
494        boolean doNotPoll() {
495            return doNotPoll;
496        }
497
498        HashMap<Integer, Dcc4PcSensor> inputPorts = new HashMap<>(16);
499
500        void addSensor(int port, Dcc4PcSensor sensor) {
501            inputPorts.put(port, sensor);
502        }
503
504        String getEncodingAsString() {
505            if ((encoding & 0x01) == 0x01) {
506                return "Supports Cooked RailCom Encoding";
507
508            } else {
509                return "Supports Raw RailCom Encoding";
510            }
511        }
512
513        void setDescription(String description) {
514            this.description = description;
515        }
516
517        Dcc4PcSensor getSensorAtIndex(int i) {
518            if (!inputPorts.containsKey(i)) {
519                return null;
520            }
521            return inputPorts.get(i);
522        }
523
524        int getNumEnabledSensors() {
525            return inputPorts.size();
526        }
527
528        HashMap<Integer, Dcc4PcReporter> inputReportersPorts = new HashMap<>(16);
529
530        void addReporter(int port, Dcc4PcReporter reporter) {
531            inputReportersPorts.put(port, reporter);
532        }
533
534        Dcc4PcReporter getReporterAtIndex(int i) {
535            if (!inputReportersPorts.containsKey(i)) {
536                return null;
537            }
538            return inputReportersPorts.get(i);
539        }
540
541        Dcc4PcRawDecoder decoder = new Dcc4PcRawDecoder(new int[0]);
542        volatile boolean synced = false;
543        volatile boolean resetPending = false;
544        private final HashMap<Integer, Integer> appliedStates = new HashMap<>(16);
545
546        void setEnabledInputs(List<Integer> enabled) {
547            int[] inputs = new int[enabled.size()];
548            for (int i = 0; i < inputs.length; i++) {
549                inputs[i] = enabled.get(i);
550            }
551            decoder = new Dcc4PcRawDecoder(inputs);
552            blockLocos = new Dcc4PcBlockLocos(inputs);
553            shownStates = new int[inputs.length];
554            pendingStates = new int[inputs.length];
555            pendingCounts = new int[inputs.length];
556            pendingSince = new long[inputs.length];
557        }
558
559        Dcc4PcBlockLocos blockLocos = new Dcc4PcBlockLocos(new int[0]);
560
561        /**
562         * A new input state is only shown once it has lasted this many packets
563         * in a row, so a one-packet glitch (seen: orientation B with garbage
564         * data on a block holding a loco without RailCom) doesn't reach the
565         * sensors, reporters or loco tracking. At about 70 packets a second
566         * this adds roughly 15 ms.
567         */
568        static final int STATE_DEBOUNCE_PACKETS = 2;
569
570        /**
571         * Going unoccupied must also last this long. A loco crossing a block
572         * gap or dirty rail drops out for a moment (seen: J5 unoccupied and
573         * back three times in half a second), and each drop would otherwise
574         * remove its RailCom tags from the block.
575         */
576        static final long UNOCCUPIED_HOLD_MS = 300;
577
578        private int[] shownStates = new int[0];
579        private int[] pendingStates = new int[0];
580        private int[] pendingCounts = new int[0];
581        private long[] pendingSince = new long[0];
582
583        /** @return the debounced states for this packet, in the decoder's input order */
584        private int[] debounce(int[] raw, long now) {
585            for (int i = 0; i < raw.length; i++) {
586                if (raw[i] == shownStates[i]) {
587                    pendingCounts[i] = 0;
588                } else if (pendingCounts[i] > 0 && raw[i] == pendingStates[i]) {
589                    pendingCounts[i]++;
590                    boolean held = raw[i] != Dcc4PcRawDecoder.STATE_UNOCCUPIED
591                            || now - pendingSince[i] >= UNOCCUPIED_HOLD_MS;
592                    if (pendingCounts[i] >= STATE_DEBOUNCE_PACKETS && held) {
593                        shownStates[i] = raw[i];
594                        pendingCounts[i] = 0;
595                    }
596                } else {
597                    pendingStates[i] = raw[i];
598                    pendingCounts[i] = 1;
599                    pendingSince[i] = now;
600                }
601            }
602            return shownStates.clone();
603        }
604
605        boolean isSynced() {
606            return synced;
607        }
608
609        /**
610         * Stop decoding and ask the reader to start its Raw encoding stream
611         * again. Decoding resumes once the reader acknowledges the Reset
612         * Encoding State.
613         *
614         * @param reason why, for the log
615         */
616        void requestResync(String reason) {
617            log.debug("Resynchronising board {}: {}", address, reason); // routine: several a minute with busy RailCom
618            synced = false;
619            sendReset();
620        }
621
622        /** Send another Reset Encoding State unless one is already waiting for its reply. */
623        void resendResetIfNeeded() {
624            if (!synced && !resetPending && !speedPending) {
625                sendReset();
626            }
627        }
628
629        volatile boolean speedChecked = false;
630        volatile boolean speedPending = false;
631        /** True after the board stopped answering at all, until a reset succeeds again. */
632        volatile boolean lostContact = false;
633        volatile long speedStarted = 0;
634        int failedResets = 0;
635
636        /** Give up on a speed change that never got an answer, so polling can start at 8 kbps. */
637        void checkSpeedTimeout() {
638            if (speedPending && System.currentTimeMillis() - speedStarted > 10000) {
639                log.warn("No answer to the speed change for board {}", address);
640                speedCheckDone(false);
641            }
642        }
643        int busSpeed = 0;
644
645        /**
646         * Move this board to 62.5 kbps: at 8 kbps its buffer overflows between
647         * polls and every Get Data takes over half a second. The board's Set
648         * Speed comes first, then the interface's, then a Get Info to check the
649         * board still answers; polling starts once that is settled.
650         */
651        private void startSpeedChange() {
652            speedPending = true;
653            speedStarted = System.currentTimeMillis();
654            synced = false;
655            log.info("Switching board {} to 62.5 kbps", address);
656            sendWakeUp();
657            tc.sendDcc4PcMessage(Dcc4PcMessage.setBoardSpeed(address, Dcc4PcMessage.SPEED_62K5), null);
658            tc.sendDcc4PcMessage(Dcc4PcMessage.busSetSpeed(address, Dcc4PcMessage.SPEED_62K5), null);
659            Dcc4PcMessage check = Dcc4PcMessage.getInfo(address);
660            check.setTimeout(1000);
661            tc.sendDcc4PcMessage(check, Dcc4PcSensorManager.this);
662        }
663
664        /**
665         * The Get Info that follows a speed change has been answered or has
666         * failed.
667         *
668         * @param ok true if the board answered at the new speed
669         */
670        void speedCheckDone(boolean ok) {
671            if (!speedPending) {
672                return;
673            }
674            if (ok) {
675                busSpeed = Dcc4PcMessage.SPEED_62K5;
676                speedRetryDelay = FIRST_SPEED_RETRY;
677                log.info("Board {} is running at 62.5 kbps", address);
678            } else {
679                busSpeed = 0;
680                // try again later: the board may only have been without track power
681                nextSpeedRetry = System.currentTimeMillis() + speedRetryDelay;
682                log.warn("Board {} did not answer at 62.5 kbps, going back to 8 kbps; trying again in {} s",
683                        address, speedRetryDelay / 1000);
684                speedRetryDelay = Math.min(speedRetryDelay * 2, MAX_SPEED_RETRY);
685                tc.sendDcc4PcMessage(Dcc4PcMessage.busResetSpeeds(), null);
686                tc.sendDcc4PcMessage(Dcc4PcMessage.busSetSpeed(address, 0), null);
687            }
688            speedChecked = true;
689            speedPending = false;
690            requestResync("bus speed set");
691        }
692
693        /**
694         * A board that has just powered up (it runs on track power) ignores
695         * every command except Get Info until it has answered a Get Info, so
696         * send one before anything else. No listener: the reply is not the
697         * speed check.
698         */
699        private void sendWakeUp() {
700            tc.sendDcc4PcMessage(Dcc4PcMessage.getInfo(address), null);
701        }
702
703        private void sendReset() {
704            resetPending = true;
705            sendWakeUp();
706            tc.sendDcc4PcMessage(Dcc4PcMessage.selectRawEncoding(address), null);
707            Dcc4PcMessage m = Dcc4PcMessage.resetBoardData(address);
708            m.setTimeout(2000);
709            tc.sendDcc4PcMessage(m, Dcc4PcSensorManager.this);
710        }
711
712        /**
713         * The reader answered a Reset Encoding State.
714         *
715         * @param ok true if it was accepted
716         */
717        void resetReplied(boolean ok) {
718            if (!ok) {
719                failedResets++;
720                log.debug("Reset Encoding State failed for board {} ({} in a row)", address, failedResets);
721                if (failedResets >= 3 && !speedPending) {
722                    // The board is powered from the track, so a short or power cut restarts
723                    // it at 8 kbps while the interface is still on the faster speed. Wait at
724                    // 8 kbps until it answers again before trying 62.5 kbps: trying while
725                    // track power was still off failed each time and doubled the retry
726                    // delay, so after a 70 s outage JMRI stayed at 8 kbps for 5 minutes.
727                    if (!lostContact) {
728                        log.warn("Board {} not answering, waiting for it at 8 kbps", address);
729                    }
730                    lostContact = true;
731                    failedResets = 0;
732                    busSpeed = 0;
733                    tc.sendDcc4PcMessage(Dcc4PcMessage.busResetSpeeds(), null);
734                    tc.sendDcc4PcMessage(Dcc4PcMessage.busSetSpeed(address, 0), null);
735                }
736                resetPending = false;
737                return;
738            }
739            failedResets = 0;
740            if (lostContact) {
741                // it's back (track power restored): change speed again straight away
742                lostContact = false;
743                log.info("Board {} is answering again", address);
744                speedRetryDelay = FIRST_SPEED_RETRY;
745                nextSpeedRetry = System.currentTimeMillis();
746            }
747            // queued behind any Get Data replies that were already on their way, which are dropped
748            packetProcessor.execute(() -> {
749                decoder.reset();
750                resetPending = false;
751                synced = true;
752                log.debug("Board {} synchronised", address);
753            });
754        }
755
756        /** Forget the old link state and change speed again from 8 kbps, after the port was reopened. */
757        void restartAfterReconnect() {
758            log.info("Setting up board {} again after the interface reconnected", address);
759            failedResets = 0;
760            resetPending = false;
761            speedPending = false;
762            lostContact = false;
763            busSpeed = 0;
764            nextSpeedRetry = 0;
765            speedRetryDelay = FIRST_SPEED_RETRY;
766            startSpeedChange();
767        }
768
769        static final long FIRST_SPEED_RETRY = 5000;
770        static final long MAX_SPEED_RETRY = 300000;
771        long speedRetryDelay = FIRST_SPEED_RETRY;
772        volatile long nextSpeedRetry = 0;
773
774        /**
775         * Called from the poll loop: if a speed change fell back to 8 kbps
776         * (often because track power was still off), try 62.5 kbps again once
777         * the retry time has come.
778         */
779        void retrySpeedIfDue() {
780            if (synced && speedChecked && !speedPending && busSpeed == 0
781                    && nextSpeedRetry != 0 && System.currentTimeMillis() >= nextSpeedRetry) {
782                nextSpeedRetry = 0;
783                startSpeedChange();
784            }
785        }
786
787        /**
788         * Decode one Get Data reply and update the sensors and reporters.
789         * Called on the packet processor thread, in the order the replies
790         * arrived.
791         *
792         * @param r the reply
793         */
794        void processRawReply(Dcc4PcReply r) {
795            if (!synced) {
796                log.debug("Board {} not synchronised, dropping {}", address, r.toHexString());
797                return;
798            }
799            int[] data = new int[r.getNumDataElements()];
800            for (int i = 0; i < data.length; i++) {
801                data[i] = r.getElement(i) & 0xFF;
802            }
803            for (Dcc4PcRawDecoder.Packet p : decoder.decode(data)) {
804                switch (p.kind) {
805                    case OVERFLOW:
806                        log.debug("Board {} buffer overflowed, packets lost", address);
807                        forgetPomPackets();
808                        break;
809                    case UNKNOWN_COMMAND:
810                        requestResync("unknown command in Raw encoding stream");
811                        return;
812                    default:
813                        applyPacket(p);
814                        break;
815                }
816            }
817        }
818
819        private void applyPacket(Dcc4PcRawDecoder.Packet p) {
820            int[] inputs = decoder.getInputs();
821            long now = System.currentTimeMillis();
822            int[] states = debounce(p.states, now);
823            for (int i = 0; i < inputs.length; i++) {
824                int state = sensorState(states[i]);
825                Integer old = appliedStates.put(inputs[i], state);
826                if (old != null && old == state) {
827                    continue;
828                }
829                Dcc4PcSensor s = getSensorAtIndex(inputs[i]);
830                if (s != null) {
831                    s.setOwnState(state);
832                }
833                Dcc4PcReporter rep = getReporterAtIndex(inputs[i]);
834                if (rep != null) {
835                    rep.setRailComState(state);
836                }
837            }
838            // which locos are in which block, from channel 2 replies and clean channel 1 addresses
839            for (Dcc4PcBlockLocos.Change c : blockLocos.apply(p, states, now)) {
840                if (!c.refresh) {
841                    log.debug("Board {}: {}", address, c); // each time a loco enters or leaves a block
842                }
843                Dcc4PcReporter rep = getReporterAtIndex(c.input);
844                if (rep == null) {
845                    continue;
846                }
847                int a = c.address & ~Dcc4PcBlockLocos.LONG;
848                if (c.refresh) {
849                    rep.locoHeard(a);
850                } else if (c.added) {
851                    rep.locoEntered(a, (c.address & Dcc4PcBlockLocos.LONG) != 0);
852                } else {
853                    rep.locoExited(a);
854                }
855            }
856            for (Dcc4PcRawDecoder.InputData d : p.railCom) {
857                if (d.data == null || d.sourceDcc == null) {
858                    continue; // a duplicate of data lost to an overflow
859                }
860                Dcc4PcReporter rep = getReporterAtIndex(d.input);
861                if (rep == null) {
862                    continue;
863                }
864                try {
865                    decodeDCCPacket(d.sourceDcc, now);
866                } catch (RuntimeException ex) {
867                    log.error("decodeDCCPacket Exception", ex);
868                    continue;
869                }
870                rep.setPacket(d.data, dcc_addr_type, addr, cvNumber, speed, 0);
871            }
872        }
873
874        private int sensorState(int rawState) {
875            switch (rawState) {
876                case Dcc4PcRawDecoder.STATE_UNOCCUPIED:
877                    return Sensor.INACTIVE;
878                case Dcc4PcRawDecoder.STATE_OCCUPIED:
879                    return Sensor.ACTIVE;
880                case Dcc4PcRawDecoder.STATE_ORIENTATION_A:
881                    return Dcc4PcSensor.ORIENTA;
882                default:
883                    return Dcc4PcSensor.ORIENTB;
884            }
885        }
886
887        int addr = 0;
888        int dcc_addr_type = NO_ADDRESS;
889        int cvNumber = 0;
890        int speed = 0;
891
892        int[] lastDCCPacketSeen = new int[0];
893
894        /**
895         * How long after a POM packet is seen a POM answer that follows
896         * another packet to the same loco is still credited to that POM
897         * packet's CV. Answers normally come 160-270 ms after the packet.
898         */
899        static final long POM_ANSWER_WINDOW = 1000;
900
901        /**
902         * How many cutouts to a loco, starting with the one after the first
903         * POM packet for a new CV, may still carry the decoder's previous
904         * answer and so aren't credited. Usually only the first does, but
905         * the decoder can take longer, or the reader can miss a POM packet
906         * (2026-10-16: 1.1.284 was given CV 283's value once with 1).
907         */
908        static final int STALE_CUTOUTS = 2;
909
910        /**
911         * Per loco (see {@link #locoKey}): the CV of the last POM packet seen,
912         * when, and how many more of its cutouts aren't credited.
913         */
914        private final HashMap<Integer, long[]> lastPom = new HashMap<>();
915
916        /** When the POM packet whose CV {@link #cvNumber} was credited from was seen; -1 if none. */
917        private long creditedPomTime = -1;
918
919        private int locoKey() {
920            return (dcc_addr_type << 16) | addr;
921        }
922
923        /**
924         * Forget the POM packets seen so far, after packets were lost to an
925         * overflow. A lost packet may have been a newer POM packet, so a
926         * later answer may be for a CV we never saw; crediting it to the
927         * last one seen would give that CV the wrong answer (2026-10-15).
928         * Answers count again from the next POM packet seen.
929         */
930        void forgetPomPackets() {
931            lastPom.clear();
932            if (creditedPomTime >= 0) {
933                cvNumber = 0;
934                creditedPomTime = -1;
935            }
936        }
937
938        void decodeDCCPacket(int[] packet) {
939            decodeDCCPacket(packet, System.currentTimeMillis());
940        }
941
942        /**
943         * Decode the DCC packet a RailCom cutout followed: sets
944         * {@link #addr}, {@link #dcc_addr_type} and {@link #cvNumber}.
945         * <p>
946         * A decoder may answer a POM packet in the cutout after a later
947         * packet to it, not the POM packet itself. With only one loco in
948         * the command station's refresh that is nearly always the case
949         * (2026-10-15: the answer to "write CV 31" followed a speed packet).
950         * So after any packet to a loco, {@link #cvNumber} is the CV of the
951         * last POM packet seen for it, if that was within
952         * {@link #POM_ANSWER_WINDOW}.
953         * <p>
954         * Except the first {@link #STALE_CUTOUTS} cutouts to the loco from the
955         * first POM packet for a new CV: there the decoder may still send its
956         * previous answer (2026-10-16, ESU LokSound 5: usually exactly one
957         * stale cutout). Crediting them gave every CV the value of the one
958         * read before. After an overflow, any POM packet counts as new, as a
959         * newer one may have been lost.
960         *
961         * @param packet the DCC packet bytes
962         * @param now    the current time in ms
963         */
964        void decodeDCCPacket(int[] packet, long now) {
965            if (Arrays.equals(packet, lastDCCPacketSeen)) {
966                if (creditedPomTime >= 0 && now - creditedPomTime > POM_ANSWER_WINDOW) {
967                    cvNumber = 0; // the same packet again, but too long after the POM packet
968                }
969                return;
970            }
971            lastDCCPacketSeen = packet.clone();
972            speed = 0;
973            addr = 0;
974            cvNumber = 0; // only a POM packet, or a recent one to the same loco, sets it
975            creditedPomTime = -1;
976            dcc_addr_type = NO_ADDRESS;
977            if (packet.length < 2) {
978                return; // too short to hold an address and an instruction
979            }
980            if (log.isDebugEnabled()) {
981
982                StringBuilder buf = new StringBuilder();
983                for (int i = 0; i < packet.length; ++i) {
984                    buf.append(Integer.toHexString(packet[i])).append(",");
985                }
986                String s = buf.toString();
987                log.debug("bytes to process {}", s);
988            }
989            // Basic Accessory Decoder packet 10aaaaaa 1aaacddd eeeeeeee
990            // Extended Accessory decoder packet starts 10aaaaaa 0aaa0aa1 000xxxxx eeeeeeee
991            int i = 0;
992            // Skip accessory decoder packets at this point
993            if ((packet[i] & 0xFF) == 0x00) {
994                return;
995                //Broadcast packet
996            } else if ((packet[i] & 0xff) == 0xff) {
997                // log.debug("Idle Packet");
998                return;
999            } else if ((packet[i] & 0xC0) == 0x80) {
1000                // Accessory decoder packet (10aaaaaa). Testing only the top bit also
1001                // caught long loco addresses (11aaaaaa), so their POM replies and DYN
1002                // data never reached the loco.
1003                return;
1004            } else {
1005                //The way that this determins of there is a two part extended address packet isn't great./
1006                int addr_1 = packet[i] & 0xFF; // packets shorter than 2 bytes were dropped above
1007                i++;
1008                if (addr_1 == 0) {
1009                    dcc_addr_type = NO_ADDRESS;
1010                } else if ((addr_1 >= 1) && (addr_1 <= 127)) { //Short address
1011                    dcc_addr_type = SHORT_ADDRESS;
1012                    addr = addr_1;
1013                } else if ((addr_1 >= 128) && (addr_1 <= 191)) {
1014                    dcc_addr_type = NO_ADDRESS; //this is an accessory decoder address should have already of been filtered out
1015                } else if ((addr_1 >= 192) && (addr_1 <= 231)) { //14 bit address
1016                    dcc_addr_type = LONG_ADDRESS;
1017                    addr = (((addr_1 & 0x3F) << 8) | (packet[i] & 0xFF));
1018                    i++;
1019                } else {
1020                    dcc_addr_type = NO_ADDRESS;
1021                }
1022                String addt;
1023                switch (dcc_addr_type) {
1024                    case LONG_ADDRESS:
1025                        addt = "Long";
1026                        break;
1027                    case SHORT_ADDRESS:
1028                        addt = "Short";
1029                        break;
1030                    default:
1031                        addt = "No Address";
1032                        break;
1033                }
1034
1035                log.debug("DCC address type {} addr {}", addt, addr);
1036                if ((dcc_addr_type != NO_ADDRESS)) {
1037                    long[] pom = lastPom.get(locoKey());
1038                    boolean isPom = (packet[i] & 0xE0) == 0xE0;
1039                    if (!isPom && pom != null && now - pom[1] <= POM_ANSWER_WINDOW) {
1040                        if (pom[2] > 0) {
1041                            pom[2]--; // may still be the previous CV's answer
1042                        } else {
1043                            cvNumber = (int) pom[0];
1044                            creditedPomTime = pom[1];
1045                        }
1046                    }
1047                    log.debug("Current index  {} value {}", i, packet[i] & 0xFF);
1048                    if (isPom) {
1049                        // 1110CCVV VVVVVVVV: the instruction byte holds the CV number's top two bits
1050                        int cvHigh = packet[i] & 0x03;
1051                        i++;
1052                        int pomCv = ((cvHigh << 8) | (packet[i] & 0xff)) + 1;
1053                        creditedPomTime = -1;
1054                        if (pom == null || pom[0] != pomCv) {
1055                            // the decoder answers this cutout, and maybe the next, before it
1056                            // has acted on the packet
1057                            lastPom.put(locoKey(), new long[]{pomCv, now, STALE_CUTOUTS - 1});
1058                            log.debug("CV Access cv:{}, first packet; its answer is the previous CV's", pomCv);
1059                        } else {
1060                            pom[1] = now;
1061                            if (pom[2] > 0) {
1062                                pom[2]--;
1063                            } else {
1064                                cvNumber = pomCv;
1065                            }
1066                            log.debug("CV Access cv:{}", pomCv);
1067                        }
1068                        // two byte instruction when 1111
1069                        // three byte instuction when 1110
1070                    } else if ((packet[i] & 0xC0) == 0xC0) {
1071                        log.debug("Future");
1072                        i++;
1073                        if ((packet[i] & 0x1F) == 0x1F) {
1074                            // F21-F28 Two byte instruction
1075                            i++;
1076                        } else if ((packet[i] & 0x1E) == 0x1E) {
1077                            // F13-F20 Two byte instruction
1078                            i++;
1079                        } else if ((packet[i] & 0x1D) == 0x1D) {
1080                            // Two byte instruction
1081                            i++;
1082
1083                        } else if ((packet[i] >> 3) == 0x00) {
1084                            // This should fall through to 00000 three byte instruction
1085                            // i = i + 2;
1086
1087                        } else {
1088                            // Remainder are reserved
1089                        }
1090                        // Two or three byte instruction
1091                    } else if ((packet[i] & 0xA0) == 0xa0) {
1092                        log.debug("For Function Group 2");
1093                        if ((packet[i] & 0x10) == 0x10) {
1094                            log.debug("Functions 5 to 8");
1095                            if ((packet[i] & 0x08) == 0x08) {
1096                                log.debug("Function 8 on");
1097                            } else {
1098                                log.debug("Function 8 off");
1099                            }
1100                            if ((packet[i] & 0x04) == 0x04) {
1101                                log.debug("Function 7 on");
1102                            } else {
1103                                log.debug("Function 7 off");
1104                            }
1105                            if ((packet[i] & 0x02) == 0x02) {
1106                                log.debug("Function 6 on");
1107                            } else {
1108                                log.debug("Function 6 off");
1109                            }
1110                            if ((packet[i] & 0x01) == 0x01) {
1111                                log.debug("Function 5 on");
1112                            } else {
1113                                log.debug("Function 5 off");
1114                            }
1115                        } else {
1116                            log.debug("Functions 9 to 12");
1117                            if ((packet[i] & 0x08) == 0x08) {
1118                                log.debug("Function 12 on");
1119                            } else {
1120                                log.debug("Function 12 off");
1121                            }
1122                            if ((packet[i] & 0x04) == 0x04) {
1123                                log.debug("Function 11 on");
1124                            } else {
1125                                log.debug("Function 11 off");
1126                            }
1127                            if ((packet[i] & 0x02) == 0x02) {
1128                                log.debug("Function 10 on");
1129                            } else {
1130                                log.debug("Function 10 off");
1131                            }
1132                            if ((packet[i] & 0x01) == 0x01) {
1133                                log.debug("Function 9 on");
1134                            } else {
1135                                log.debug("Function 9 off");
1136                            }
1137                        }
1138                        // Single byte instruction
1139                        // i++;
1140                    } else if ((packet[i] & 0x80) == 0x80) {
1141                        log.debug("For Function Group 1");
1142                    } else if ((packet[i] & 0x60) == 0x60) {
1143                        speed = (packet[i] & 0xff) - 0x60;
1144                        log.debug("Speed for forward 14 speed steps {}", speed);
1145                        //Only a single byte instruction
1146                    } else if ((packet[i] & 0x40) == 0x40) {
1147                        speed = ((packet[i] & 0xff) - 0x40);
1148                        log.debug("Speed for reverse 14 speed steps {}", speed);
1149                        //Only a single byte instruction
1150                    } else if ((packet[i] & 0x20) == 0x20) {
1151                        log.debug("Advanced Op");
1152                        //Two byte instruction
1153                        if ((packet[i] & 0x1f) == 0x1f) {
1154                            i++;
1155                            log.debug("128 speed step control");
1156                            if ((packet[i] & 0x80) == 0x80) {
1157                                speed = ((packet[i]) & 0xff) - 0x80;
1158                                log.debug("Forward {}", speed);
1159                            } else {
1160                                speed = ((packet[i]) & 0xff);
1161                                log.debug("Reverse {}", speed);
1162                            }
1163                        }
1164                    } else {
1165                        log.debug("Decoder and Consist Instruction");
1166                        // decoder control is 0000
1167                        // consist control is 0001
1168                    }
1169                }
1170
1171            }
1172            log.debug("---End Decode DCC Packet---");
1173        }
1174
1175        int getInputs() {
1176            return inputs;
1177        }
1178
1179        String getDescription() {
1180            return description;
1181        }
1182
1183        String getVersion() {
1184            return version;
1185        }
1186    }
1187
1188    public List<Integer> getBoards() {
1189        ArrayList<Integer> list = new ArrayList<>();
1190        Enumeration<Integer> keys = activeBoards.keys();
1191        while (keys.hasMoreElements()) {
1192            Integer key = keys.nextElement();
1193            list.add(key);
1194        } // end while
1195        return list;
1196    }
1197
1198    public int getBoardInputs(int board) {
1199        if (!activeBoards.containsKey(board)) {
1200            return -1;
1201        }
1202        return activeBoards.get(board).getInputs();
1203    }
1204
1205    public String getBoardEncodingAsString(int board) {
1206        if (!activeBoards.containsKey(board)) {
1207            return "unknown";
1208        }
1209        return activeBoards.get(board).getEncodingAsString();
1210    }
1211
1212    public String getBoardVersion(int board) {
1213        if (!activeBoards.containsKey(board)) {
1214            return "unknown";
1215        }
1216        return activeBoards.get(board).getVersion();
1217    }
1218
1219    public String getBoardDescription(int board) {
1220        if (!activeBoards.containsKey(board)) {
1221            return "unknown";
1222        }
1223        return activeBoards.get(board).getDescription();
1224    }
1225
1226    protected boolean isBoardCreated(int address) {
1227        return activeBoards.containsKey(address);
1228    }
1229
1230    protected void addActiveBoard(int address, String version, int inputs, int encoding) {
1231        activeBoards.put(address, new ActiveBoard(address, version, inputs, encoding));
1232    }
1233
1234    protected void setBoardDescription(int address, String description) {
1235        ActiveBoard board = activeBoards.get(address);
1236        board.setDescription(description);
1237    }
1238
1239    protected void createSensorsForBoard(Dcc4PcReply r) {
1240        if (r.getBoard() == -1) {
1241            log.debug("Reply has no board associated with it");
1242            return;
1243        }
1244        class SensorMaker implements Runnable {
1245
1246            Dcc4PcReply reply;
1247
1248            SensorMaker(Dcc4PcReply r) {
1249                reply = r;
1250            }
1251
1252            @Override
1253            public void run() {
1254                createSensorsFromReply(reply);
1255            }
1256        }
1257
1258        Thread thr = new Thread(new SensorMaker(r), "Dcc4PCSensor Maker board " + r.getBoard());
1259        try {
1260            thr.start();
1261        } catch (java.lang.IllegalThreadStateException ex) {
1262            log.error("Exception {} {}", thr.getName(), ex.getMessage());
1263        }
1264    }
1265
1266    /**
1267     * Validates to contain at least 1 number . . .
1268     * <p>
1269     * TODO: Custom validation for Dcc4PcSensorManager could be improved.
1270     * {@inheritDoc}
1271     */
1272    @Override
1273    @Nonnull
1274    public String validateSystemNameFormat(@Nonnull String name, @Nonnull java.util.Locale locale) throws jmri.NamedBean.BadSystemNameException {
1275        return validateTrimmedMin1NumberSystemNameFormat(name,locale);
1276    }
1277
1278    @Override
1279    public void dispose() {
1280        jmri.InstanceManager.getDefault(jmri.ShutDownManager.class).deregister(pollShutDownTask);
1281        stopPolling();
1282        super.dispose();
1283    }
1284
1285    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(Dcc4PcSensorManager.class);
1286
1287}