001package jmri.jmrix.dcc4pc;
002
003import java.io.DataInputStream;
004import java.lang.reflect.InvocationTargetException;
005import java.util.Calendar;
006
007import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
008
009import jmri.jmrix.AbstractMRListener;
010import jmri.jmrix.AbstractMRMessage;
011import jmri.jmrix.AbstractMRReply;
012import jmri.jmrix.AbstractMRTrafficController;
013import jmri.jmrix.dcc4pc.serialdriver.SerialDriverAdapter;
014
015import org.slf4j.Logger;
016import org.slf4j.LoggerFactory;
017
018/**
019 * Converts Stream-based I/O to/from DCC4PC messages. The "Dcc4PcInterface" side
020 * sends/receives message objects.
021 * <p>
022 * The connection to a Dcc4PcPortController is via a pair of *Streams, which
023 * then carry sequences of characters for transmission. Note that this
024 * processing is handled in an independent thread.
025 * <p>
026 * This handles the state transitions, based on the necessary state in each
027 * message.
028 *
029 * @author Bob Jacobsen Copyright (C) 2001
030 */
031public class Dcc4PcTrafficController extends AbstractMRTrafficController implements Dcc4PcInterface {
032
033    /**
034     * Create a new DccPcTrafficController instance.
035     */
036    public Dcc4PcTrafficController() {
037        super();
038        if (log.isDebugEnabled()) {
039            log.debug("creating a new Dcc4PcTrafficController object");
040        }
041        this.setAllowUnexpectedReply(false);
042    }
043
044    public void setAdapterMemo(Dcc4PcSystemConnectionMemo memo) {
045        adaptermemo = memo;
046    }
047
048    Dcc4PcSystemConnectionMemo adaptermemo;
049
050    @Override
051    public synchronized void addDcc4PcListener(Dcc4PcListener l) {
052        this.addListener(l);
053    }
054
055    @Override
056    public synchronized void removeDcc4PcListener(Dcc4PcListener l) {
057        this.removeListener(l);
058    }
059
060    /** Why this class locks the base class's xmtRunnable, which SpotBugs flags as visible outside the class. */
061    private static final String XMT_LOCK = "The base class waits and notifies on xmtRunnable, so the receive"
062            + " and transmit threads must synchronise on that same object to wake each other";
063
064    public static final int RETRIEVINGDATA = 100;
065
066    /**
067     * Forward a Dcc4PcMessage to all registered Dcc4PcInterface listeners.
068     */
069    @Override
070    protected void forwardMessage(AbstractMRListener client, AbstractMRMessage m) {
071        ((Dcc4PcListener) client).message((Dcc4PcMessage) m);
072    }
073
074    /**
075     * Forward a Dcc4PcReply to all registered Dcc4PcInterface listeners.
076     */
077    @Override
078    protected void forwardReply(AbstractMRListener client, AbstractMRReply r) {
079        ((Dcc4PcListener) client).reply((Dcc4PcReply) r);
080    }
081
082    @Override
083    protected AbstractMRMessage pollMessage() {
084        return null;
085    }
086
087    @Override
088    protected AbstractMRListener pollReplyHandler() {
089        return null;
090    }
091
092    /**
093     * Forward a preformatted message to the actual interface.
094     */
095    @Override
096    public void sendDcc4PcMessage(Dcc4PcMessage m, Dcc4PcListener reply) {
097        sendMessage(m, reply);
098    }
099
100    protected boolean unsolicitedSensorMessageSeen = false;
101
102    //Dcc4Pc doesn't support this function.
103    @Override
104    protected AbstractMRMessage enterProgMode() {
105        return Dcc4PcMessage.getProgMode();
106    }
107
108    // DCC4PC has no programming mode, so there's nothing to send. The old
109    // one-byte 0x00 message was the interface's own Get Info, and at shutdown
110    // terminate() sent it with 100 retries, flooding the log.
111    @Override
112    protected AbstractMRMessage enterNormalMode() {
113        return null;
114    }
115
116    /**
117     * At shutdown, put every board and the interface back to 8 kbps so other
118     * programs can talk to them. The next JMRI start resets the speed anyway.
119     */
120    @Override
121    protected void terminate() {
122        log.debug("Cleanup Starts");
123        stopJamWatchdog();
124        if (ostream == null || port == null) {
125            return;    // no connection established
126        }
127        // let a reply that is still being fetched finish first
128        long end = System.currentTimeMillis() + 1000;
129        while (sequenceInProgress && System.currentTimeMillis() < end) {
130            try {
131                Thread.sleep(5);
132            } catch (InterruptedException e) {
133                Thread.currentThread().interrupt();
134                break;
135            }
136        }
137        Dcc4PcMessage m = Dcc4PcMessage.busResetSpeeds();
138        m.setRetries(0);
139        // Don't wait on a write that will never finish: with the USB link jammed,
140        // waiting here would stop JMRI quitting.
141        boolean locked = false;
142        try {
143            locked = !xmtException && portLock.tryLock(1, java.util.concurrent.TimeUnit.SECONDS);
144        } catch (InterruptedException e) {
145            Thread.currentThread().interrupt();
146        }
147        if (!locked) {
148            log.warn("DCC4PC interface not responding; bus speeds not reset at quit");
149            return;
150        }
151        try {
152            forwardToPort(m, null);
153        } finally {
154            portLock.unlock();
155        }
156    }
157
158    /**
159     * Held while writing to the port, by the transmit thread and by the
160     * receive thread (which sends Get Response itself), so the two never
161     * write at once. It's a separate lock, not this object's monitor, because
162     * a write can block for a long time when the interface's USB link jams;
163     * the GUI takes the monitor (to notify listeners and queue messages), and
164     * holding it during the write froze the whole window.
165     */
166    private final java.util.concurrent.locks.ReentrantLock portLock = new java.util.concurrent.locks.ReentrantLock();
167
168    /** When the current write started, or 0 if no write is in progress. */
169    private volatile long writeStartedAt = 0;
170
171    /** A write that hasn't finished after this long means the USB link has jammed. */
172    static final long STUCK_WRITE_MILLIS = 3000;
173
174    private volatile boolean jamReported = false;
175    private java.util.TimerTask jamWatchdog;
176
177    /**
178     * Watch for a write that never finishes. jSerialComm's write timeout
179     * doesn't always fire: on 2026-10-12 a write stayed blocked for over 10
180     * minutes, until the interface was unplugged. Call with portLock held.
181     */
182    private void startJamWatchdog() {
183        if (jamWatchdog != null) {
184            return;
185        }
186        jamWatchdog = new java.util.TimerTask() {
187            @Override
188            public void run() {
189                long started = writeStartedAt;
190                if (started != 0 && System.currentTimeMillis() - started > STUCK_WRITE_MILLIS) {
191                    reportJam("a write to the port has not finished after " + STUCK_WRITE_MILLIS / 1000 + " s");
192                }
193            }
194        };
195        jmri.util.TimerUtil.schedule(jamWatchdog, 1000, 1000);
196    }
197
198    private void stopJamWatchdog() {
199        java.util.TimerTask t = jamWatchdog;
200        if (t != null) {
201            t.cancel();
202        }
203    }
204
205    /**
206     * {@inheritDoc}
207     */
208    @Override
209    public void terminateThreads() {
210        stopJamWatchdog();
211        Thread t = recoveryThread;
212        if (t != null) {
213            t.interrupt();
214        }
215        super.terminateThreads();
216    }
217
218    /**
219     * Tell the user, once per failure, that the interface has stopped
220     * responding and how to recover. Killing JMRI while a write is stuck in
221     * the USB driver has crashed Windows (bugcheck 0x1A), so the advice is to
222     * unplug first.
223     */
224    private void reportJam(String why) {
225        if (jamReported) {
226            return;
227        }
228        jamReported = true;
229        String text = "The DCC4PC interface has stopped responding (" + why + ").\n"
230                + "Unplug its USB cable and plug it back in: JMRI will reconnect by itself.\n"
231                + "Don't end JMRI from Task Manager while the interface is still plugged in.";
232        log.error("The DCC4PC interface has stopped responding ({}). Unplug its USB cable and plug it back in:"
233                + " JMRI will reconnect by itself. Don't end JMRI from Task Manager while the interface is still plugged in.", why);
234        if (!java.awt.GraphicsEnvironment.isHeadless()) {
235            jmri.util.swing.JmriJOptionPane.showMessageDialogNonModal(null, text,
236                    "DCC4PC interface not responding", jmri.util.swing.JmriJOptionPane.ERROR_MESSAGE, null);
237        }
238    }
239
240    /**
241     * Run on the layout thread once the port has been reopened after a
242     * failure. The interface restarts at 8 kbps when it is plugged back in, so
243     * the managers must set the bus up again as they do at startup.
244     */
245    private volatile Runnable reconnectHandler;
246
247    public void setReconnectHandler(Runnable handler) {
248        reconnectHandler = handler;
249    }
250
251    /** True from a port failure until the port has been reopened. */
252    volatile boolean recovering = false;
253    private volatile Thread recoveryThread;
254    private final Object recoveryLock = new Object();
255
256    /** How often to check whether the port is back. */
257    long recoveryPollMillis = 1000;
258
259    /** Longest wait between attempts to reopen a port that is listed but won't open. */
260    static final long MAX_REOPEN_DELAY = 30000;
261
262    /**
263     * Reading or writing the port has failed, usually because the interface's
264     * USB link jammed and was then unplugged. Stop sending, tell the user, and
265     * reopen the port once it is back (issue 26).
266     */
267    private void portFailed(String why) {
268        xmtException = true;
269        reportJam(why);
270        if (!(controller instanceof SerialDriverAdapter)) {
271            return;
272        }
273        synchronized (recoveryLock) {
274            if (recovering || threadStopRequest) {
275                return;
276            }
277            recovering = true;
278            Thread t = new Thread(() -> recoverPort((SerialDriverAdapter) controller), "DCC4PC port recovery");
279            t.setDaemon(true);
280            recoveryThread = t;
281            t.start();
282        }
283    }
284
285    /**
286     * Close the failed port, wait until the operating system lists it again
287     * (it disappears while the interface is unplugged), then reopen it.
288     * Runs on its own thread.
289     */
290    private void recoverPort(SerialDriverAdapter adapter) {
291        String portName = adapter.getCurrentPortName();
292        jmri.jmrix.ConnectionStatus.instance().setConnectionState(adapter.getSystemConnectionMemo(),
293                jmri.jmrix.ConnectionStatus.CONNECTION_DOWN);
294        log.info("Closing {}; it will be reopened when the interface is back", portName);
295        try {
296            closePort(adapter, portName);
297            boolean wasGone = false;
298            long delay = recoveryPollMillis;
299            while (!threadStopRequest) {
300                Thread.sleep(delay);
301                if (!adapter.isPortPresent()) {
302                    if (!wasGone) {
303                        log.info("{} has gone; waiting for the interface to be plugged back in", portName);
304                        wasGone = true;
305                    }
306                    delay = recoveryPollMillis;
307                    continue;
308                }
309                if (wasGone) {
310                    // just plugged back in: give the system a moment to finish setting it up
311                    Thread.sleep(2 * recoveryPollMillis);
312                    wasGone = false;
313                }
314                String error = adapter.openPort(portName, "JMRI");
315                if (error == null) {
316                    reattach(adapter, portName);
317                    return;
318                }
319                delay = Math.min(Math.max(delay, recoveryPollMillis) * 2, MAX_REOPEN_DELAY);
320                log.warn("Could not reopen {} ({}); trying again in {} s", portName, error, delay / 1000);
321            }
322        } catch (InterruptedException e) {
323            Thread.currentThread().interrupt();
324            log.debug("Port recovery stopped");
325        }
326    }
327
328    /**
329     * Close the port on a separate thread, in case the driver blocks: a write
330     * stuck in a jammed USB driver has held on to the port before.
331     */
332    private void closePort(SerialDriverAdapter adapter, String portName) throws InterruptedException {
333        Thread closer = new Thread(adapter::closeSerialPort, "DCC4PC port close");
334        closer.setDaemon(true);
335        closer.start();
336        closer.join(2000);
337        if (closer.isAlive()) {
338            log.warn("{} did not close within 2 s; trying to reopen it anyway", portName);
339        }
340    }
341
342    /** Switch to the reopened port's streams, and start sending again. */
343    private void reattach(SerialDriverAdapter adapter, String portName) throws InterruptedException {
344        portLock.lockInterruptibly(); // a write may still be finishing on the old port
345        try {
346            istream = adapter.getInputStream();
347            ostream = adapter.getOutputStream();
348            port = adapter.getSerialPort();
349            lastIncomplete = null;
350            waitingForMore = false;
351            sequenceInProgress = false;
352            mLastMessage = null;
353            mLastSentMessage = null;
354            rcvException = false;
355            xmtException = false;
356            jamReported = false;
357        } finally {
358            portLock.unlock();
359        }
360        // whatever was queued during the outage is stale; the managers start again from scratch
361        synchronized (this) {
362            msgQueue.clear();
363            listenerQueue.clear();
364        }
365        recovering = false;
366        jmri.jmrix.ConnectionStatus.instance().setConnectionState(adapter.getSystemConnectionMemo(),
367                jmri.jmrix.ConnectionStatus.CONNECTION_UP);
368        log.info("Reopened {}; setting up the DCC4PC bus again", portName);
369        Runnable handler = reconnectHandler;
370        if (handler != null) {
371            jmri.util.ThreadingUtil.runOnLayoutEventually(handler::run);
372        }
373    }
374
375    @Override
376    protected void addTrailerToOutput(byte[] msg, int offset, AbstractMRMessage m) {
377    }
378
379    Dcc4PcMessage mLastMessage;  //Last message requested with a reply listener ie from external methods
380    Dcc4PcMessage mLastSentMessage; //Last message actually sent from within the code, ie getResponse.
381
382    // Not synchronized on this object: see portLock.
383    @Override
384    protected void forwardToPort(AbstractMRMessage m, AbstractMRListener reply) {
385        if (log.isDebugEnabled()) {
386            log.debug("forwardToPort message: [{}]", m);
387        }
388        if (port == null) {
389            return;
390        }
391        portLock.lock();
392        try {
393            // remember who sent this
394            mLastSender = reply;
395            mLastMessage = (Dcc4PcMessage) m;
396
397            // forward the message to the registered recipients,
398            // which includes the communications monitor, except the sender.
399            // Schedule notification via the Swing event queue to ensure order
400            if (!mLastMessage.isGetResponse()) {
401                //Do not forward on the get response packets, saves filling up the monitors with chaff
402                Runnable r = new XmtNotifier(m, mLastSender, this);
403                javax.swing.SwingUtilities.invokeLater(r);
404            }
405            forwardToPort(m);
406        } finally {
407            portLock.unlock();
408        }
409    }
410
411    //this forward to port is also used internally for repeating commands.
412    private void forwardToPort(AbstractMRMessage m) {
413        portLock.lock();
414        try {
415            writeToPort(m);
416        } finally {
417            portLock.unlock();
418        }
419    }
420
421    // Call with portLock held.
422    @SuppressFBWarnings(value = "USO_UNSAFE_OBJECT_SYNCHRONIZATION", justification = XMT_LOCK)
423    private void writeToPort(AbstractMRMessage m) {
424        if (xmtException) {
425            // A write has failed and the port is being reopened, so the retry
426            // loop below would only wait out each message's timeouts.
427            return;
428        }
429        startJamWatchdog();
430        mLastSentMessage = (Dcc4PcMessage) m;
431        // stream to port in single write, as that's needed by serial
432        byte msg[] = new byte[lengthOfByteStream(m)];
433
434        // add data content
435        int len = m.getNumDataElements();
436        for (int i = 0; i < len; i++) {
437            msg[i] = (byte) m.getElement(i);
438        }
439
440        try {
441            if (ostream != null) {
442                if (log.isDebugEnabled()) {
443                    StringBuilder f = new StringBuilder();
444                    for (int i = 0; i < msg.length; i++) {
445                        f.append(Integer.toHexString(0xFF & msg[i]));
446                        f.append(" ");
447                    }
448                    log.debug("formatted message: {}", f);
449                }
450                while (m.getRetries() >= 0) {
451                    if (portReadyToSend(controller)) {
452                        // DSR high means the interface is still answering the last command.
453                        // (It also reads high just after the port opens, so don't wait long.)
454                        waitForDsrLow(200, false);
455                        waitForGap();
456                        port.setDTR(true);
457                        writeStartedAt = System.currentTimeMillis();
458                        try {
459                            ostream.write(msg);
460                        } finally {
461                            writeStartedAt = 0;
462                        }
463                        try {
464                            Thread.sleep(20);
465                        } catch (InterruptedException ex) {
466                            Thread.currentThread().interrupt();
467                        } catch (Exception ex) {
468                            log.warn("sendMessage: Exception: {}", ex.toString());
469                        }
470                        ostream.flush();
471                        port.setDTR(false);
472                        if (((Dcc4PcMessage) m).isNoDataReply()) {
473                            waitForDsrLow(m.getTimeout(), true);
474                            lastTransactionEnd = System.currentTimeMillis();
475                        }
476                        break;
477                    } else if (m.getRetries() >= 0) {
478                        log.debug("Retry message: {} attempts remaining: {}", m, m.getRetries());
479                        m.setRetries(m.getRetries() - 1);
480                        try {
481                            synchronized (xmtRunnable) {
482                                xmtRunnable.wait(m.getTimeout());
483                            }
484                        } catch (InterruptedException e) {
485                            Thread.currentThread().interrupt(); // retain if needed later
486                            log.error("retry wait interrupted");
487                        }
488                    } else {
489                        log.warn("sendMessage: port not ready for data sending: {}", java.util.Arrays.toString(msg));
490                    }
491                }
492            } else {
493                // ostream is null
494                // no stream connected
495                connectionWarn();
496            }
497        } catch (java.io.IOException | RuntimeException e) {
498            portWarn(e);
499            portFailed("a write to the port failed: " + e.getMessage());
500        }
501    }
502    /**
503     * Wait for the interface to finish a command that it answers with no data:
504     * DSR falls when it is done, usually within 2 ms of DTR going low.
505     *
506     * @param timeout longest wait, in milliseconds
507     * @param warn    log a warning if DSR is still high at the end
508     */
509    private void waitForDsrLow(int timeout, boolean warn) {
510        long end = System.currentTimeMillis() + timeout;
511        while (port.isDSR()) {
512            if (System.currentTimeMillis() > end) {
513                if (warn) {
514                    log.warn("Interface still busy (DSR high) {} ms after {}", timeout, mLastSentMessage);
515                } else {
516                    log.debug("DSR still high after {} ms, sending anyway", timeout);
517                }
518                return;
519            }
520            try {
521                Thread.sleep(1);
522            } catch (InterruptedException ex) {
523                Thread.currentThread().interrupt();
524                return;
525            }
526        }
527    }
528
529    /**
530     * Quiet time between the end of one transaction and the start of the
531     * next. Sending each Get Response straight after the last reply ended
532     * jammed the interface's USB link after a few minutes of polling with two
533     * locos; our standalone tools always leave about this long, and ran for
534     * minutes without a jam.
535     */
536    static final int MIN_GAP_MS = 20;
537
538    volatile long lastTransactionEnd = 0;
539
540    private void waitForGap() {
541        long wait = lastTransactionEnd + MIN_GAP_MS - System.currentTimeMillis();
542        if (wait > 0) {
543            try {
544                Thread.sleep(wait);
545            } catch (InterruptedException ex) {
546                Thread.currentThread().interrupt();
547            }
548        }
549    }
550
551    volatile jmri.jmrix.purejavacomm.SerialPort port;
552
553    @Override
554    public void connectPort(jmri.jmrix.AbstractPortController p) {
555
556        super.connectPort(p);
557        port = ((SerialDriverAdapter) controller).getSerialPort();
558
559    }
560
561    /**
562     * Like the base class's loop, but a failed port doesn't end it: the port
563     * is reopened (see {@link #portFailed}) and reading carries on.
564     */
565    @Override
566    public void receiveLoop() {
567        log.debug("receiveLoop starts in {}", this);
568        int errorCount = 0;
569        while (!threadStopRequest) {
570            if (recovering) {
571                try {
572                    Thread.sleep(50);
573                } catch (InterruptedException e) {
574                    break;
575                }
576                continue;
577            }
578            try {
579                handleOneIncomingReply();
580                errorCount = 0;
581            } catch (java.io.InterruptedIOException e) {
582                if (!recovering) {
583                    break; // asked to stop
584                }
585            } catch (java.io.IOException e) {
586                if (!threadStopRequest) {
587                    portFailed("reading from the port failed: " + e.getMessage());
588                }
589            } catch (RuntimeException e) {
590                log.error("Exception in receive loop: {}", e.toString(), e);
591                if (++errorCount >= maxRcvExceptionCount) {
592                    errorCount = 0;
593                    portFailed("too many errors reading from the port");
594                }
595            }
596        }
597        log.debug("receiveLoop ends");
598    }
599
600    @Override
601    protected AbstractMRReply newReply() {
602        Dcc4PcReply reply = new Dcc4PcReply();
603        return reply;
604    }
605
606    // for now, receive always OK
607    @Override
608    protected boolean canReceive() {
609        return true;
610    }
611
612    @Override
613    protected boolean endOfMessage(AbstractMRReply msg) {
614        if (port.isDSR()) {
615            return false;
616        }
617        try {
618            if (controller.getInputStream().available() > 0) {
619                if (port.isRI()) {
620                    log.debug("??? Ringing true ???");
621                }
622                return false;
623            }
624
625            //log.debug("No more input available " + port.isDSR());
626            if (port.isRI()) {
627                log.debug("??? Ringing true ???");
628            }
629            return true;
630        } catch (java.io.IOException ex) {
631            log.error("IO Exception{}", ex.toString());
632        }
633        return !port.isDSR();
634    }
635
636    @Override
637    protected void handleTimeout(AbstractMRMessage msg, AbstractMRListener l) {
638        if(l != null){
639            ((Dcc4PcListener) l).handleTimeout((Dcc4PcMessage) msg);
640        }
641        super.handleTimeout(msg, l);
642    }
643
644    Dcc4PcReply lastIncomplete;
645    boolean waitingForMore = false;
646    boolean loading = false;
647
648    // True while the receive thread is fetching a reply with Get Response; the
649    // transmit thread must not send another command until it has finished.
650    volatile boolean sequenceInProgress = false;
651
652    /** Longest a single reply sequence may hold up the transmit thread. */
653    static final int MAX_SEQUENCE_TIME = 10000;
654
655    final int GETMOREDATA = 0x01;
656
657    /**
658     * Handle each reply when complete.
659     * <p>
660     * (This is public for testing purposes) Runs in the "Receive" thread.
661     *
662     */
663    @Override
664    @SuppressFBWarnings(value = "USO_UNSAFE_OBJECT_SYNCHRONIZATION", justification = XMT_LOCK)
665    public void handleOneIncomingReply() throws java.io.IOException {
666        // we sit in this until the message is complete, relying on
667        // threading to let other stuff happen
668
669        // Create message off the right concrete class
670        AbstractMRReply msg = newReply();
671
672        // message exists, now fill it
673        loadChars(msg, istream);
674        if (mLastSentMessage != null) {
675            ((Dcc4PcReply)msg).setOriginalRequest(mLastMessage);
676            //log.debug(mLastMessage.getElement(0));
677            if (mLastSentMessage.isForChildBoard()) {
678                if (log.isDebugEnabled()) {
679                    log.debug("This is a message for a child board {}", ((Dcc4PcReply) msg).toHexString());
680                    log.debug("Originate {}", mLastMessage.toString());
681                }
682                if ((mLastSentMessage.getNumDataElements() - 1) == msg.getElement(1)) {
683                    log.debug("message lengths match");
684                    waitingForMore = true;
685                    try {
686                        Thread.sleep(10);
687                    } catch (InterruptedException ex) {
688                        log.debug("InterruptedException", ex);
689                    }
690                    //log.debug("We do not forward the response to the listener as it has not been formed");
691                    lastIncomplete = null;
692                    sequenceInProgress = true;
693                    forwardToPort(Dcc4PcMessage.getResponse());
694
695                    return;
696                } else {
697                    if (log.isDebugEnabled()) {
698                        log.debug("Not all of the command was sent, we need to figure out a way to resend the bits");
699                        log.debug("Original Message length {}", mLastSentMessage.getNumDataElements());
700                        log.debug("What CID has procced in size {}", (byte) msg.getElement(1));
701                        log.debug("Reply is in error {}", ((Dcc4PcReply) msg).toHexString());
702                    }
703                }
704            } else if (mLastSentMessage.getElement(0) == 0x0C) {
705                if (log.isDebugEnabled()) {
706                    log.debug("last message was a get response {}", ((Dcc4PcReply) msg).toHexString());
707                }
708                if (msg.getElement(0) == Dcc4PcReply.SUCCESS) {
709                    ((Dcc4PcReply) msg).strip();
710                    if (lastIncomplete != null) {
711                        //log.debug("Need to add the new reply to this message");
712                        //log.debug("existing : " + lastIncomplete.toHexString());
713
714                        //Append this message to the last incomplete message
715                        if (msg.getNumDataElements() != 0) {
716                            int iOrig = lastIncomplete.getNumDataElements();
717                            int iNew = 0;
718                            while (iNew < msg.getNumDataElements()) {
719                                lastIncomplete.setElement(iOrig, msg.getElement(iNew));
720                                iOrig++;
721                                iNew++;
722                            }
723                        }
724                        //set the last incomplete message as the one to return
725                        log.debug("Reply set as lastIncomplete");
726                        msg = lastIncomplete;
727                    }
728                    ((Dcc4PcReply) msg).setError(false);
729                    ((Dcc4PcReply)msg).setOriginalRequest(mLastMessage);
730                    lastIncomplete = null;
731                    waitingForMore = false;
732                    mLastMessage = null;
733                    mLastSentMessage = null;
734                } else if (msg.getElement(0) == Dcc4PcReply.INCOMPLETE) {
735                    waitingForMore = true;
736                    ((Dcc4PcReply) msg).strip();
737                    if (lastIncomplete != null) {
738                        //Append this message to the last incomplete message
739                        if (msg.getNumDataElements() != 0) {
740                            int iOrig = lastIncomplete.getNumDataElements();
741                            int iNew = 0;
742                            while (iNew < msg.getNumDataElements()) {
743                                lastIncomplete.setElement(iOrig, msg.getElement(iNew));
744                                iOrig++;
745                                iNew++;
746                            }
747                        }
748
749                    } else if (msg.getNumDataElements() > 0) { // a first chunk of one byte is data too
750                        lastIncomplete = (Dcc4PcReply) msg;
751                    }
752                    //We do not forward the response to the listener as it has not been formed
753                    sequenceInProgress = true;
754                    forwardToPort(Dcc4PcMessage.getResponse());
755
756                    return;
757
758                } else {
759                    log.debug("Reply is an error mesage");
760                    ((Dcc4PcReply) msg).setError(true);
761                    mLastMessage.setRetries(mLastMessage.getRetries() - 1);
762                    if (mLastMessage.getRetries() >= 0) {
763                        sequenceInProgress = false;
764                        synchronized (xmtRunnable) {
765                            mCurrentState = AUTORETRYSTATE;
766                            replyInDispatch = false;
767                            xmtRunnable.notify();
768                        }
769                        return;
770                    }
771                }
772            }
773        } else {
774            log.debug("Last message sent was null {}", ((Dcc4PcReply) msg).toHexString());
775        }
776
777        // message is complete, dispatch it !!
778        sequenceInProgress = false;
779        replyInDispatch = true;
780        if (log.isDebugEnabled()) {
781            log.debug("dispatch reply of length {} contains {} state {}", msg.getNumDataElements(), msg.toString(), mCurrentState);
782        }
783        // forward the message to the registered recipients,
784        // which includes the communications monitor
785        // return a notification via the Swing event queue to ensure proper thread
786        Runnable r = newRcvNotifier(msg, mLastSender, this);
787        try {
788            javax.swing.SwingUtilities.invokeAndWait(r);
789        } catch (InterruptedException | InvocationTargetException e) {
790            log.error("Unexpected exception in invokeAndWait:", e);
791        }
792
793        if (log.isDebugEnabled()) {
794            log.debug("dispatch thread invoked");
795        }
796        if (!msg.isUnsolicited()) {
797            // effect on transmit:
798            switch (mCurrentState) {
799                case WAITMSGREPLYSTATE: {
800                    // check to see if the response was an error message we want
801                    // to automatically handle by re-queueing the last sent
802                    // message, otherwise go on to the next message
803                    if (msg.isRetransmittableErrorMsg()) {
804                        if (log.isDebugEnabled()) {
805                            log.debug("Automatic Recovery from Error Message: {}", msg.toString());
806                        }
807                        synchronized (xmtRunnable) {
808                            mCurrentState = AUTORETRYSTATE;
809                            replyInDispatch = false;
810                            xmtRunnable.notify();
811                        }
812                    } else {
813                        // update state, and notify to continue
814                        synchronized (xmtRunnable) {
815                            mCurrentState = NOTIFIEDSTATE;
816                            replyInDispatch = false;
817                            xmtRunnable.notify();
818                        }
819                    }
820                    break;
821                }
822                case WAITREPLYINPROGMODESTATE: {
823                    // entering programming mode
824                    mCurrentMode = PROGRAMINGMODE;
825                    replyInDispatch = false;
826
827                    // check to see if we need to delay to allow decoders to become
828                    // responsive
829                    int warmUpDelay = enterProgModeDelayTime();
830                    if (warmUpDelay != 0) {
831                        try {
832                            synchronized (xmtRunnable) {
833                                xmtRunnable.wait(warmUpDelay);
834                            }
835                        } catch (InterruptedException e) {
836                            Thread.currentThread().interrupt(); // retain if needed later
837                        }
838                    }
839                    // update state, and notify to continue
840                    synchronized (xmtRunnable) {
841                        mCurrentState = OKSENDMSGSTATE;
842                        xmtRunnable.notify();
843                    }
844                    break;
845                }
846                case WAITREPLYINNORMMODESTATE: {
847                    // entering normal mode
848                    mCurrentMode = NORMALMODE;
849                    replyInDispatch = false;
850                    // update state, and notify to continue
851                    synchronized (xmtRunnable) {
852                        mCurrentState = OKSENDMSGSTATE;
853                        xmtRunnable.notify();
854                    }
855                    break;
856                }
857                default: {
858                    replyInDispatch = false;
859                    unexpectedReplyStateError(mCurrentState,msg.toString());
860                }
861            }
862            // Unsolicited message
863        } else {
864            if (log.isDebugEnabled()) {
865                log.debug("Unsolicited Message Received {}", msg.toString());
866            }
867            replyInDispatch = false;
868        }
869    }
870
871    boolean normalFlushReceiveChars = false;
872
873    //Need a way to detect that the dsr has gone low.
874    @Override
875    protected void loadChars(AbstractMRReply msg, DataInputStream istream)
876            throws java.io.IOException {
877        int i;
878        readingData = false;
879        MAINGET:
880        {
881            for (i = 0; i < msg.maxSize(); i++) {
882                boolean waiting = true;
883                while (waiting) {
884                    if (recovering) {
885                        // the port is closed or about to be; the receive loop waits for it to reopen
886                        throw new java.io.InterruptedIOException("port being reopened");
887                    }
888                    if (controller.getInputStream().available() > 0) {
889                        readingData = true;
890                        byte char1 = readByteProtected(istream);
891                        waiting = false;
892
893                        //potentially add in a flush here that is generated by the transmit after a command has been sent, but this is not an error type flush.l
894                        // if there was a timeout, flush any char received and start over
895                        if (flushReceiveChars) {
896                            lastIncomplete = null;
897                            waitingForMore = false;
898                            sequenceInProgress = false;
899                            mLastMessage = null;
900                            mLastSentMessage = null;
901                            readingData = false;
902                            log.warn("timeout flushes receive buffer: {}", ((Dcc4PcReply) msg).toHexString());
903                            msg.flush();
904                            i = 0;  // restart
905                            flushReceiveChars = false;
906                            waiting = true;
907                        } else {
908                            if (canReceive()) {
909                                if (log.isDebugEnabled()) {
910                                    log.debug("Set data {}, {}", i, char1 & 0xff);
911                                }
912                                msg.setElement(i, char1);
913                                waiting = false;
914                                if (port.isRI()) {
915                                    log.debug("Ring high error");
916                                    ((Dcc4PcReply) msg).setError(true);
917                                    break MAINGET;
918                                }
919                                if (endOfMessage(msg)) {
920                                    break MAINGET;
921                                }
922                            } else {
923                                i--; // flush char
924                                log.error("unsolicited character received: {}", Integer.toHexString(char1));
925                            }
926                        }
927                    } else if (!port.isDSR()) {
928                        if (i == 0) {
929                            waiting = true;
930                            idleWait();
931                        } else {
932                            log.debug("We have data so will break");
933                            waiting = false;
934                            break MAINGET;
935                        }
936                    } else {
937                        //As we have no data to process we will set the readingData flag false;
938                        readingData = false;
939                        // Only pause before the reply starts. Once its bytes are arriving, read
940                        // each one as soon as it's there: pausing mid-reply read the bytes late,
941                        // and was the likely cause of replies cut short (issue 29).
942                        if (i == 0) {
943                            idleWait();
944                        }
945                    }
946                }
947            }
948        }
949        lastTransactionEnd = System.currentTimeMillis();
950    }
951
952    /**
953     * Pause briefly while loadChars is waiting for a reply to start. Without
954     * it the receive thread polled available() and DSR non-stop, using a whole
955     * CPU core whenever JMRI was idle (issue 29). It isn't used once a reply's
956     * bytes are arriving: on Windows a 1 ms sleep can last up to ~15 ms, and
957     * with it the Raw stream misaligned (7 times in one 5-minute run, against
958     * none without it).
959     *
960     * @throws java.io.InterruptedIOException if the thread is interrupted or
961     *                                        asked to stop, which ends the
962     *                                        receive loop
963     */
964    private void idleWait() throws java.io.InterruptedIOException {
965        if (threadStopRequest) {
966            throw new java.io.InterruptedIOException("receive thread stopping");
967        }
968        try {
969            Thread.sleep(1);
970        } catch (InterruptedException ex) {
971            Thread.currentThread().interrupt();
972            throw new java.io.InterruptedIOException("receive thread interrupted");
973        }
974    }
975
976    boolean readingData = false;
977
978    @Override
979    @SuppressFBWarnings(value = "USO_UNSAFE_OBJECT_SYNCHRONIZATION", justification = XMT_LOCK)
980    protected void transmitWait(int waitTime, int state, String InterruptMessage) {
981        // wait() can have spurious wakeup!
982        // so we protect by making sure the entire timeout time is used
983        long currentTime = Calendar.getInstance().getTimeInMillis();
984        long endTime = currentTime + waitTime;
985        long hardEnd = currentTime + Math.max(waitTime, MAX_SEQUENCE_TIME);
986        while (endTime > (currentTime = Calendar.getInstance().getTimeInMillis())) {
987            long wait = endTime - currentTime;
988            try {
989                synchronized (xmtRunnable) {
990                    // Do not wait if the current state has changed since we
991                    // last set it.
992
993                    if (mCurrentState != state) {
994                        return;
995                    }
996                    xmtRunnable.wait(wait); // rcvr normally ends this w state change
997                    //If we are in the process of reading the data then do not time out.
998                    if (readingData) {
999                        endTime = endTime + 10;
1000                    }
1001                    //if we have received a packet and a seperate message has been sent to retrieve
1002                    //the reply we will add more time to our wait process.
1003                    if (waitingForMore) {
1004                        waitingForMore = false;
1005                        //if we are in the process of retrieving data, then we shall increase the endTime by 200ms.
1006                        endTime = endTime + 200;
1007                    }
1008                    // Timing out part way through a Get Response sequence would let the next
1009                    // command interrupt it, which can jam the interface's USB link.
1010                    long now = Calendar.getInstance().getTimeInMillis();
1011                    if (sequenceInProgress && endTime <= now + 50 && now < hardEnd) {
1012                        endTime = now + 50;
1013                    }
1014                }
1015            } catch (InterruptedException e) {
1016                Thread.currentThread().interrupt(); // retain if needed later
1017                log.error("{} from {}", InterruptMessage, e.getMessage());
1018            }
1019        }
1020        log.debug("TIMEOUT in transmitWait, mCurrentState:{} {} port dsr {} wait time {}", mCurrentState, state, port.isDSR(), waitTime);
1021    }
1022
1023    private static final Logger log = LoggerFactory.getLogger(Dcc4PcTrafficController.class);
1024}