001package jmri.jmrix;
002
003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
004import java.io.DataInputStream;
005import java.io.IOException;
006import java.io.OutputStream;
007import java.util.*;
008
009import javax.annotation.Nonnull;
010import javax.swing.SwingUtilities;
011
012import jmri.InstanceManager;
013import jmri.ShutDownManager;
014
015/**
016 * Abstract base for TrafficControllers in a Message/Reply protocol.
017 * <p>
018 * Two threads are used for the actual communication. The "Transmit" thread
019 * handles pushing characters to the port, and also changing the mode. The
020 * "Receive" thread converts characters from the input stream into replies.
021 * <p>
022 * The constructor registers a shutdown task to
023 * trigger the necessary cleanup code
024 * <p>
025 * The internal state machine handles changes of mode, automatic retry of
026 * certain messages, time outs, and sending poll messages when otherwise idle.
027 * <p>
028 * "Mode" refers to the state of the command station communications. "Normal"
029 * and "Programming" are the two modes, used if the command station requires
030 * messages to go back and forth between them. <br>
031 *
032 * <img src="doc-files/AbstractMRTrafficController-StateDiagram.png" alt="UML State diagram">
033 *
034 * <p>
035 * The key methods for the basic operation are:
036 * <ul>
037 * <li>If needed for formatting outbound messages, {@link #addHeaderToOutput(byte[], AbstractMRMessage)} and {@link #addTrailerToOutput(byte[], int, AbstractMRMessage)}
038 * <li> {@link #newReply()} creates an empty reply message (of the proper concrete type) to fill with incoming data
039 * <li>The {@link #endOfMessage(AbstractMRReply) } method is used to parse incoming messages. If it needs
040 *      information on e.g. the last message sent, that can be stored in member variables
041 *      by {@link #forwardToPort(AbstractMRMessage, AbstractMRListener)}.
042 *  <li>{@link #forwardMessage(AbstractMRListener, AbstractMRMessage)} and {@link #forwardReply(AbstractMRListener, AbstractMRReply) } handle forwarding of specific types of objects
043 * </ul>
044 * <p>
045 * If your command station requires messages to go in and out of
046 * "programming mode", those should be provided by
047 * {@link #enterProgMode()} and {@link #enterNormalMode()}.
048 * <p>
049 * If you want to poll for information when the line is otherwise idle,
050 * implement {@link #pollMessage()} and {@link #pollReplyHandler()}.
051 *
052 * @author Bob Jacobsen Copyright (C) 2003
053 * @author Paul Bender Copyright (C) 2004-2010
054 */
055
056/*
057@startuml jmri/jmrix/doc-files/AbstractMRTrafficController-StateDiagram.png
058
059    [*] --> IDLESTATE
060    IDLESTATE --> NOTIFIEDSTATE : sendMessage()
061    NOTIFIEDSTATE --> IDLESTATE : queue empty
062
063    NOTIFIEDSTATE --> WAITMSGREPLYSTATE : transmitLoop()\nwake, send message
064
065    WAITMSGREPLYSTATE --> WAITREPLYINPROGMODESTATE : transmitLoop()\nnot in PROGRAMINGMODE,\nmsg for PROGRAMINGMODE
066    WAITMSGREPLYSTATE --> WAITREPLYINNORMMODESTATE : transmitLoop()\nnot in NORMALMODE,\nmsg for NORMALMODE
067
068    WAITMSGREPLYSTATE --> NOTIFIEDSTATE : handleOneIncomingReply()
069
070    WAITREPLYINPROGMODESTATE --> OKSENDMSGSTATE : handleOneIncomingReply()\nentered PROGRAMINGMODE
071    WAITREPLYINNORMMODESTATE --> OKSENDMSGSTATE : handleOneIncomingReply()\nentered NORMALMODE
072    OKSENDMSGSTATE --> WAITMSGREPLYSTATE : send original pended message
073
074    IDLESTATE --> POLLSTATE : transmitLoop()\nno work
075    POLLSTATE --> WAITMSGREPLYSTATE : transmitLoop()\npoll msg exists, send it
076    POLLSTATE --> IDLESTATE : transmitLoop()\nno poll msg to send
077
078    WAITMSGREPLYSTATE --> AUTORETRYSTATE : handleOneIncomingReply()\nwhen tagged as error reply
079    AUTORETRYSTATE --> IDLESTATE : to drive a repeat of a message
080
081NOTIFIEDSTATE : Transmit thread wakes up and processes
082POLLSTATE : Transient while deciding to send poll
083OKSENDMSGSTATE : Transient while deciding to send\noriginal message after mode change
084AUTORETRYSTATE : Transient while deciding to resend auto-retry message
085WAITREPLYINPROGMODESTATE : Sent request to go to programming mode,\nwaiting reply
086WAITREPLYINNORMMODESTATE : Sent request to go to normal mode,\nwaiting reply
087WAITMSGREPLYSTATE : Have sent message, waiting a\nresponse from layout
088
089Note left of AUTORETRYSTATE : This state handles timeout of\nmessages marked for autoretry
090Note left of OKSENDMSGSTATE : Transient internal state\nwill transition when going back\nto send message that\nwas deferred for mode change.
091
092@enduml
093 */
094
095public abstract class AbstractMRTrafficController {
096
097    private final Runnable shutDownTask = this::terminate; // retain for possible removal.
098
099    /**
100     * Create a new unnamed MRTrafficController.
101     */
102    public AbstractMRTrafficController() {
103        log.debug("Creating AbstractMRTrafficController instance");
104        mCurrentMode = NORMALMODE;
105        mCurrentState = IDLESTATE;
106        allowUnexpectedReply = false;
107
108
109        // We use a shutdown task here to make sure the connection is left
110        // in a clean state prior to exiting.  This is required on systems
111        // which have a service mode to ensure we don't leave the system
112        // in an unusable state. Once the shutdown task executes, the connection
113        // must be considered permanently closed.
114
115        InstanceManager.getDefault(ShutDownManager.class).register(shutDownTask);
116    }
117
118    private boolean synchronizeRx = true;
119
120    protected void setSynchronizeRx(boolean val) {
121        synchronizeRx = val;
122    }
123
124    protected boolean getSynchronizeRx() {
125        return synchronizeRx;
126    }
127
128    // The methods to implement the abstract Interface
129
130    protected final Vector<AbstractMRListener> cmdListeners = new Vector<>();
131
132    /**
133     * Add a Listener to the Listener list.
134     * @param l The Listener to be added, not null.
135     */
136    protected synchronized void addListener(AbstractMRListener l) {
137        // add only if not already registered
138        if (l == null) {
139            throw new NullPointerException();
140        }
141        if (!cmdListeners.contains(l)) {
142            cmdListeners.addElement(l);
143        }
144    }
145
146    /**
147     * Add a Listener to start of the Listener list.
148     * Intended for use only by system Consoles which may prefer notification
149     * before other objects have processed a Message and sent a Reply.
150     * @param l The Listener to be added, not null.
151     */
152    protected synchronized void addConsoleListener(@Nonnull AbstractMRListener l){
153        // add only if not already registered
154        if (!cmdListeners.contains(l)) {
155            cmdListeners.insertElementAt(l, 0);
156        }
157    }
158
159    /**
160     * Remove a Listener from the Listener list.
161     * The Listener will receive no further notifications.
162     * @param l The Listener to be removed.
163     */
164    protected synchronized void removeListener(AbstractMRListener l) {
165        if (cmdListeners.contains(l)) {
166            cmdListeners.removeElement(l);
167        }
168    }
169
170    /**
171     * Forward a Message to registered listeners.
172     *
173     * @param m     Message to be forwarded intact
174     * @param notMe One (optional) listener to be skipped, usually because it's
175     *              the originating object.
176     */
177    @SuppressWarnings("unchecked")
178    protected void notifyMessage(AbstractMRMessage m, AbstractMRListener notMe) {
179        // make a copy of the listener vector to synchronized not needed for transmit
180        Vector<AbstractMRListener> v;
181        synchronized (this) {
182            // FIXME: unnecessary synchronized; the Vector IS already thread-safe.
183            v = (Vector<AbstractMRListener>) cmdListeners.clone();
184        }
185        // forward to all listeners
186        int cnt = v.size();
187        for (int i = 0; i < cnt; i++) {
188            AbstractMRListener client = v.elementAt(i);
189            if (notMe != client) {
190                log.debug("notify message, client: {}", client);
191                try {
192                    forwardMessage(client, m);
193                } catch (RuntimeException e) {
194                    log.warn("notify: During message dispatch to {}", client, e);
195                }
196            }
197        }
198    }
199
200    /**
201     * Implement this to forward a specific message type to a protocol-specific
202     * listener interface.
203     * This puts the casting into the concrete class.
204     * @param client abstract listener.
205     * @param m message to forward.
206     */
207    protected abstract void forwardMessage(AbstractMRListener client, AbstractMRMessage m);
208
209    /**
210     * Invoked if it's appropriate to do low-priority polling of the command
211     * station, this should return the next message to send, or null if the
212     * TrafficController should just sleep.
213     * @return Formatted poll message
214     */
215    protected abstract AbstractMRMessage pollMessage();
216
217    protected abstract AbstractMRListener pollReplyHandler();
218
219    // Written by the transmit thread when a message is handed to the port, read
220    // by the receive thread to decide who the next reply belongs to.  It has to
221    // be volatile: without it the receive thread may keep observing a stale
222    // sender and deliver replies to a listener which is no longer the one
223    // waiting for them.
224    protected volatile AbstractMRListener mLastSender = null;
225
226    protected volatile int mCurrentMode;
227    public static final int NORMALMODE = 1;
228    public static final int PROGRAMINGMODE = 4;
229
230    /**
231     * Set the system to programming mode.
232     * @see #enterNormalMode()
233     *
234     * @return any message that needs to be returned to the Command Station
235     * to change modes. If no message is needed, returns null.
236     */
237    protected abstract AbstractMRMessage enterProgMode();
238
239    /**
240     * Sets the system to normal mode during programming while in IDLESTATE.
241     * If {@link #programmerIdle()} returns true, enterNormalMode() is
242     * called after a timeout.
243     * @see #enterProgMode()
244     *
245     * @return any message that needs to be returned to the Command Station
246     * to change modes. If no message is needed, returns null.
247     */
248    protected abstract AbstractMRMessage enterNormalMode();
249
250    /**
251     * Check if the programmer is idle.
252     * Override in the system specific code if necessary (see notes for
253     * {@link #enterNormalMode()}.
254     *
255     * @return true if not busy programming
256     */
257    protected boolean programmerIdle() {
258        return true;
259    }
260
261    /**
262     * Get the delay (wait time) after enabling the programming track.
263     * Override in subclass to add a longer delay.
264     *
265     * @return 0 as default delay
266     */
267    protected int enterProgModeDelayTime() {
268        return 0;
269    }
270
271    protected volatile int mCurrentState;
272    public static final int IDLESTATE = 10;        // nothing happened
273    public static final int NOTIFIEDSTATE = 15;    // xmt notified, will next wake
274    public static final int WAITMSGREPLYSTATE = 25;  // xmt has sent, await reply to message
275    public static final int WAITREPLYINPROGMODESTATE = 30;  // xmt has done mode change, await reply
276    public static final int WAITREPLYINNORMMODESTATE = 35;  // xmt has done mode change, await reply
277    public static final int OKSENDMSGSTATE = 40;        // mode change reply here, send original msg
278    public static final int AUTORETRYSTATE = 45;        // received message where automatic recovery may occur with a retransmission, re-send original msg
279    public static final int POLLSTATE = 50;   // Send program mode or poll message
280
281    protected boolean allowUnexpectedReply;
282
283    /**
284     * Set whether the command station may send messages without a request
285     * sent to it.
286     *
287     * @param expected true to allow messages without a prior request
288     */
289    protected void setAllowUnexpectedReply(boolean expected) {
290        allowUnexpectedReply = expected;
291    }
292
293    /**
294     * Forward a "Reply" from layout to registered listeners.
295     *
296     * @param r    Reply to be forwarded intact
297     * @param dest One (optional) listener to be skipped, usually because it's
298     *             the originating object.
299     */
300    @SuppressWarnings("unchecked")
301    protected void notifyReply(AbstractMRReply r, AbstractMRListener dest) {
302        // make a copy of the listener vector to synchronized (not needed for transmit?)
303        Vector<AbstractMRListener> v;
304        synchronized (this) {
305            // FIXME: unnecessary synchronized; the Vector IS already thread-safe.
306            v = (Vector<AbstractMRListener>) cmdListeners.clone();
307        }
308        // forward to all listeners
309        int cnt = v.size();
310        for (int i = 0; i < cnt; i++) {
311            AbstractMRListener client = v.elementAt(i);
312            log.debug("notify reply, client: {}", client);
313            try {
314                //skip dest for now, we'll send the message to there last.
315                if (dest != client) {
316                    forwardReply(client, r);
317                }
318            } catch (RuntimeException e) {
319                log.warn("notify: During reply dispatch to {}", client, e);
320            }
321        }
322
323        // forward to the last listener who sent a message
324        // this is done _second_ so monitoring can have already stored the reply
325        // before a response is sent
326        if (dest != null) {
327            log.debug("notify reply, dest: {}", dest);
328            forwardReply(dest, r);
329        }
330    }
331
332    protected abstract void forwardReply(AbstractMRListener client, AbstractMRReply m);
333
334    /**
335     * Report whether this traffic controller is at rest: no message waiting for
336     * a reply, and nothing left to send.
337     * <p>
338     * A listener still waiting for a reply while this returns true is waiting
339     * for something which will never arrive, because the exchange it belongs to
340     * has already been closed by another reply. Intended for watchdogs and
341     * diagnostics: the answer is a best effort snapshot taken without locking,
342     * not a state guaranteed to hold beyond the call.
343     *
344     * @return true if nothing is outstanding and nothing is queued
345     */
346    public boolean isAtRest() {
347        int state = mCurrentState;
348        return (state == IDLESTATE || state == NOTIFIEDSTATE) && msgQueue.isEmpty();
349    }
350
351    /**
352     * Messages to be transmitted.
353     */
354    protected LinkedList<AbstractMRMessage> msgQueue = new LinkedList<>();
355    protected LinkedList<AbstractMRListener> listenerQueue = new LinkedList<>();
356
357    /**
358     * Forward message to the port. Messages are queued and then the
359     * transmission thread is notified.
360     * @see #forwardToPort(AbstractMRMessage, AbstractMRListener)
361     *
362     * @param m the message to send
363     * @param reply the Listener sending the message, often provided as 'this'
364     */
365    protected synchronized void sendMessage(AbstractMRMessage m, AbstractMRListener reply) {
366        msgQueue.addLast(m);
367        listenerQueue.addLast(reply);
368        synchronized (xmtRunnable) {
369            if (mCurrentState == IDLESTATE) {
370                mCurrentState = NOTIFIEDSTATE;
371                xmtRunnable.notify();
372            }
373        }
374        if (m != null) {
375            log.debug("just notified transmit thread with message {}", m);
376        }
377    }
378
379    /**
380     * Permanent loop for the transmit thread.
381     */
382    protected void transmitLoop() {
383        log.debug("transmitLoop starts in {}", this);
384
385        // loop forever
386        while (!connectionError && !threadStopRequest) {
387            AbstractMRMessage m = null;
388            AbstractMRListener l = null;
389            // check for something to do
390            synchronized (this) {
391                if (!msgQueue.isEmpty()) {
392                    // yes, something to do
393                    m = msgQueue.getFirst();
394                    msgQueue.removeFirst();
395                    l = listenerQueue.getFirst();
396                    listenerQueue.removeFirst();
397                    mCurrentState = WAITMSGREPLYSTATE;
398                    log.debug("transmit loop has something to do: {}", m);
399                } else if (mCurrentState != IDLESTATE) {
400                    // Nothing to send. Go IDLE while still holding the lock
401                    // that sendMessage() holds while queueing, so that a
402                    // message queued from now on is guaranteed to see IDLE and
403                    // notify this thread. Setting IDLE after releasing the lock
404                    // overwrote the NOTIFIEDSTATE of a message queued in
405                    // between, and that message then waited out the whole
406                    // poll interval (about 100 ms) before being sent.
407                    log.debug("Setting IDLESTATE, current mode {}", mCurrentMode);
408                    mCurrentState = IDLESTATE;
409                }  // release lock here to proceed in parallel
410            }
411            // if a message has been extracted, process it
412            if (m != null) {
413                // check for need to change mode
414                log.debug("Start msg, state = {}", mCurrentMode);
415                if (m.getNeededMode() != mCurrentMode) {
416                    AbstractMRMessage modeMsg;
417                    if (m.getNeededMode() == PROGRAMINGMODE) {
418                        // change state to programming mode and send message
419                        modeMsg = enterProgMode();
420                        if (modeMsg != null) {
421                            mCurrentState = WAITREPLYINPROGMODESTATE;
422                            log.debug("Enter Programming Mode");
423                            forwardToPort(modeMsg, null);
424                            // wait for reply
425                            transmitWait(m.getTimeout(), WAITREPLYINPROGMODESTATE, "enter programming mode interrupted");
426                        }
427                    } else {
428                        // change state to normal and send message
429                        modeMsg = enterNormalMode();
430                        if (modeMsg != null) {
431                            mCurrentState = WAITREPLYINNORMMODESTATE;
432                            log.debug("Enter Normal Mode");
433                            forwardToPort(modeMsg, null);
434                            // wait for reply
435                            transmitWait(m.getTimeout(), WAITREPLYINNORMMODESTATE, "enter normal mode interrupted");
436                        }
437                    }
438                    if (modeMsg != null) {
439                        checkReplyInDispatch();
440                        if (mCurrentState != OKSENDMSGSTATE) {
441                            handleTimeout(modeMsg, l);
442                        }
443                        mCurrentState = WAITMSGREPLYSTATE;
444                    } else {
445                        // no mode message required, but the message
446                        // needs a different mode
447                        log.debug("Setting mode to: {}", m.getNeededMode());
448                        mCurrentMode = m.getNeededMode();
449                    }
450                }
451                forwardToPort(m, l);
452                // reply expected?
453                if (m.replyExpected()) {
454                    log.debug("reply expected is true for message {}",m);
455                    // wait for a reply, or eventually timeout
456                    transmitWait(m.getTimeout(), WAITMSGREPLYSTATE, "transmitLoop interrupted");
457                    checkReplyInDispatch();
458                    if (mCurrentState == WAITMSGREPLYSTATE) {
459                        handleTimeout(m, l);
460                    } else if (mCurrentState == AUTORETRYSTATE) {
461                        log.debug("Message added back to queue: {}", m);
462                        msgQueue.addFirst(m);
463                        listenerQueue.addFirst(l);
464                        synchronized (xmtRunnable) {
465                            mCurrentState = IDLESTATE;
466                        }
467                    } else {
468                        resetTimeout(m);
469                    }
470                } // just continue to the next message from here
471            } else {
472                // nothing to do, IDLESTATE was set above under the queue lock
473                // wait for something to send
474                if (mWaitBeforePoll > waitTimePoll || mCurrentMode == PROGRAMINGMODE) {
475                    try {
476                        long startTime = Calendar.getInstance().getTimeInMillis();
477                        synchronized (xmtRunnable) {
478                            // Only wait while still IDLE. If a message was
479                            // queued since the check above, sendMessage() has
480                            // already moved to NOTIFIEDSTATE and sent its
481                            // notify(), which nobody was waiting for.
482                            if (mCurrentState == IDLESTATE) {
483                                xmtRunnable.wait(mWaitBeforePoll);
484                            }
485                        }
486                        long endTime = Calendar.getInstance().getTimeInMillis();
487                        waitTimePoll = waitTimePoll + endTime - startTime;
488                    } catch (InterruptedException e) {
489                        Thread.currentThread().interrupt(); // retain if needed later
490                        // end of transmit loop
491                        break;
492                    }
493                }
494                // once we decide that mCurrentState is in the IDLESTATE and there's an xmt msg we must guarantee
495                // the change of mCurrentState to one of the waiting for reply states.  Therefore we need to synchronize.
496                synchronized (this) {
497                    if (mCurrentState != NOTIFIEDSTATE && mCurrentState != IDLESTATE) {
498                        log.error("left timeout in unexpected state: {}", mCurrentState);
499                    }
500                    if (mCurrentState == IDLESTATE) {
501                        mCurrentState = POLLSTATE; // this prevents other transitions from the IDLESTATE
502                    }
503                }
504                // went around with nothing to do; leave programming state if in it
505                if (mCurrentMode == PROGRAMINGMODE) {
506                    log.debug("Timeout - in service mode");
507                }
508                if (mCurrentState == POLLSTATE && mCurrentMode == PROGRAMINGMODE && programmerIdle()) {
509                    log.debug("timeout causes leaving programming mode");
510                    mCurrentState = WAITREPLYINNORMMODESTATE;
511                    AbstractMRMessage msg = enterNormalMode();
512                    // if the enterNormalMode() message is null, we
513                    // don't want to try to send it to the port.
514                    if (msg != null) {
515                        forwardToPort(msg, null);
516                        // wait for reply
517                        transmitWait(msg.getTimeout(), WAITREPLYINNORMMODESTATE, "interrupted while leaving programming mode");
518                        checkReplyInDispatch();
519                        // exit program mode timeout?
520                        if (mCurrentState == WAITREPLYINNORMMODESTATE) {
521                            // entering normal mode via timeout
522                            handleTimeout(msg, l);
523                            mCurrentMode = NORMALMODE;
524                        }
525                        // and go around again
526                    }
527                } else if (mCurrentState == POLLSTATE && mCurrentMode == NORMALMODE) {
528                    // We may need to poll
529                    AbstractMRMessage msg = pollMessage();
530                    if (msg != null) {
531                        // yes, send that
532                        log.debug("Sending poll, wait time {}", waitTimePoll);
533                        mCurrentState = WAITMSGREPLYSTATE;
534                        forwardToPort(msg, pollReplyHandler());
535                        // wait for reply
536                        log.debug("Still waiting for reply");
537                        transmitWait(msg.getTimeout(), WAITMSGREPLYSTATE, "interrupted while waiting poll reply");
538                        checkReplyInDispatch();
539                        // and go around again
540                        if (mCurrentState == WAITMSGREPLYSTATE) {
541                            handleTimeout(msg, l);
542                        } else {
543                            resetTimeout(msg);
544                        }
545                    }
546                    waitTimePoll = 0;
547                }
548                // no messages, so back to idle
549                if (mCurrentState == POLLSTATE) {
550                    mCurrentState = IDLESTATE;
551                }
552            }
553        }
554    }   // end of transmit loop; go around again
555
556    protected void transmitWait(int waitTime, int state, String interruptMessage) {
557        // wait() can have spurious wakeup!
558        // so we protect by making sure the entire timeout time is used
559        long currentTime = Calendar.getInstance().getTimeInMillis();
560        long endTime = currentTime + waitTime;
561        while (endTime > (currentTime = Calendar.getInstance().getTimeInMillis())) {
562            long wait = endTime - currentTime;
563            try {
564                synchronized (xmtRunnable) {
565                    // Do not wait if the current state has changed since we
566                    // last set it.
567                    if (mCurrentState != state) {
568                        return;
569                    }
570                    xmtRunnable.wait(wait); // rcvr normally ends this w state change
571                }
572            } catch (InterruptedException e) {
573                Thread.currentThread().interrupt(); // retain if needed later
574                String[] packages = this.getClass().getName().split("\\.");
575                String name = (packages.length>=2 ? packages[packages.length-2]+"." :"")
576                        +(packages.length>=1 ? packages[packages.length-1] :"");
577                if (!threadStopRequest) {
578                    log.error("{} in transmitWait(..) of {}", interruptMessage, name);
579                } else {
580                    log.debug("during shutdown, {}  in transmitWait(..) of {}", interruptMessage, name);
581                }
582            }
583        }
584        log.debug("Timeout in transmitWait, mCurrentState: {}", mCurrentState);
585    }
586
587    // Dispatch control and timer
588    protected boolean replyInDispatch = false;          // true when reply has been received but dispatch not completed
589    private int maxDispatchTime = 0;
590    private int warningMessageTime = DISPATCH_WARNING_TIME;
591    private static final int DISPATCH_WAIT_INTERVAL = 100;
592    private static final int DISPATCH_WARNING_TIME = 12000; // report warning when max dispatch time exceeded
593    private static final int WARN_NEXT_TIME = 1000;         // report every second
594
595    private void checkReplyInDispatch() {
596        int loopCount = 0;
597        while (replyInDispatch) {
598            try {
599                synchronized (xmtRunnable) {
600                    xmtRunnable.wait(DISPATCH_WAIT_INTERVAL);
601                }
602            } catch (InterruptedException e) {
603                Thread.currentThread().interrupt(); // retain if needed later
604                if (threadStopRequest) return; // don't log an error if closing.
605                String[] packages = this.getClass().getName().split("\\.");
606                String name = (packages.length>=2 ? packages[packages.length-2]+"." :"")
607                        +(packages.length>=1 ? packages[packages.length-1] :"");
608                log.error("transmitLoop interrupted in class {}", name);
609            }
610            loopCount++;
611            int currentDispatchTime = loopCount * DISPATCH_WAIT_INTERVAL;
612            if (currentDispatchTime > maxDispatchTime) {
613                maxDispatchTime = currentDispatchTime;
614                if (currentDispatchTime >= warningMessageTime) {
615                    warningMessageTime = warningMessageTime + WARN_NEXT_TIME;
616                    log.debug("Max dispatch time is now {}", currentDispatchTime);
617                }
618            }
619        }
620    }
621
622    /**
623     *  Determine if the interface is down.
624     *
625     *  @return timeoutFlag
626     */
627    public boolean hasTimeouts() {
628        return timeoutFlag;
629    }
630
631    protected boolean timeoutFlag = false;
632    protected int timeouts = 0;
633    protected boolean flushReceiveChars = false;
634
635    protected void handleTimeout(AbstractMRMessage msg, AbstractMRListener l) {
636        //log.debug("Timeout mCurrentState: {}", mCurrentState);
637        warnOnTimeout(msg, l);
638        timeouts++;
639        timeoutFlag = true;
640        flushReceiveChars = true;
641    }
642
643    protected void warnOnTimeout(AbstractMRMessage msg, AbstractMRListener l) {
644        String[] packages = this.getClass().getName().split("\\.");
645        String name = (packages.length>=2 ? packages[packages.length-2]+"." :"")
646                +(packages.length>=1 ? packages[packages.length-1] :"");
647
648        log.warn("Timeout on reply to message: {} consecutive timeouts = {} in {}", msg, timeouts, name);
649    }
650
651    protected void resetTimeout(AbstractMRMessage msg) {
652        if (timeouts > 0) {
653            log.debug("Reset timeout after {} timeouts", timeouts);
654        }
655        timeouts = 0;
656        timeoutFlag = false;
657    }
658
659    /**
660     * Add header to the outgoing byte stream.
661     *
662     * @param msg the output byte stream
663     * @param m Message results
664     * @return next location in the stream to fill
665     */
666    protected int addHeaderToOutput(byte[] msg, AbstractMRMessage m) {
667        return 0;
668    }
669
670    protected int mWaitBeforePoll = 100;
671    protected long waitTimePoll = 0;
672
673    /**
674     * Add trailer to the outgoing byte stream.
675     *
676     * @param msg    the output byte stream
677     * @param offset the first byte not yet used
678     * @param m   output message to extend
679     */
680    protected void addTrailerToOutput(byte[] msg, int offset, AbstractMRMessage m) {
681        if (!m.isBinary()) {
682            msg[offset] = 0x0d;
683        }
684    }
685
686    /**
687     * Determine how many bytes the entire message will take, including
688     * space for header and trailer.
689     *
690     * @param m the message to be sent
691     * @return number of bytes
692     */
693    protected int lengthOfByteStream(AbstractMRMessage m) {
694        int len = m.getNumDataElements();
695        int cr = 0;
696        if (!m.isBinary()) {
697            cr = 1;  // space for return char
698        }
699        return len + cr;
700    }
701
702    protected boolean xmtException = false;
703
704    /**
705     * Actually transmit the next message to the port.
706     * @see #sendMessage(AbstractMRMessage, AbstractMRListener)
707     *
708     * @param m the message to send
709     * @param reply the Listener sending the message, often provided as 'this'
710     */
711    @SuppressFBWarnings(value = {"TLW_TWO_LOCK_WAIT"},
712            justification = "Two locks needed for synchronization here, this is OK")
713    protected synchronized void forwardToPort(AbstractMRMessage m, AbstractMRListener reply) {
714        log.debug("forwardToPort message: [{}]", m);
715        // remember who sent this
716        mLastSender = reply;
717
718        // forward the message to the registered recipients,
719        // which includes the communications monitor, except the sender.
720        // Schedule notification via the Swing event queue to ensure order
721        log.trace("about to start XmtNotifier for {} last: {}", m, mLastSender, new Exception("traceback"));
722        Runnable r = new XmtNotifier(m, mLastSender, this);
723        SwingUtilities.invokeLater(r);
724
725        // stream to port in single write, as that's needed by serial
726        int byteLength = lengthOfByteStream(m);
727        byte[]  msg= new byte[byteLength];
728        log.debug("copying message, length = {}", byteLength);
729        // add header
730        int offset = addHeaderToOutput(msg, m);
731
732        // add data content
733        int len = m.getNumDataElements();
734        log.debug("copying data to message, length = {}", len);
735        if (len > byteLength) { // happens somehow
736            log.warn("Invalid message array size {} for {} elements, truncated", byteLength, len);
737        }
738        for (int i = 0; (i < len && i < byteLength); i++) {
739            msg[i + offset] = (byte) m.getElement(i);
740        }
741        // add trailer
742        addTrailerToOutput(msg, len + offset, m);
743        // and stream the bytes
744        try {
745            if (ostream != null) {
746                if (log.isDebugEnabled()) {
747                    StringBuilder f = new StringBuilder();
748                    for (int i = 0; i < msg.length; i++) {
749                        f.append(String.format("%02X ",0xFF & msg[i]));
750                    }
751                    log.debug("formatted message: {}", f.toString() );
752                }
753                while (m.getRetries() >= 0) {
754                    if (portReadyToSend(controller)) {
755                        ostream.write(msg);
756                        ostream.flush();
757                        log.debug("written, msg timeout: {} mSec", m.getTimeout());
758                        break;
759                    } else if (m.getRetries() >= 0) {
760                        log.debug("Retry message: {} attempts remaining: {}", m, m.getRetries());
761                        m.setRetries(m.getRetries() - 1);
762                        try {
763                            synchronized (xmtRunnable) {
764                                xmtRunnable.wait(m.getTimeout());
765                            }
766                        } catch (InterruptedException e) {
767                            Thread.currentThread().interrupt(); // retain if needed later
768                            log.error("retry wait interrupted");
769                        }
770                    } else {
771                        log.warn("sendMessage: port not ready for data sending: {}", Arrays.toString(msg));
772                    }
773                }
774            } else {  // ostream is null
775                // no stream connected
776                connectionWarn();
777            }
778        } catch (IOException | RuntimeException e) {
779            // TODO Currently there's no port recovery if an exception occurs
780            // must restart JMRI to clear xmtException.
781            xmtException = true;
782            portWarn(e);
783        }
784    }
785
786    protected void connectionWarn() {
787        log.warn("sendMessage: no connection established for {}", this.getClass().getName(), new Exception());
788    }
789
790    protected void portWarn(Exception e) {
791        log.warn("sendMessage: Exception: In {} port warn: ", this.getClass().getName(), e);
792    }
793
794    protected boolean connectionError = false;
795
796    protected void portWarnTCP(Exception e) {
797        log.warn("Exception java net: ", e);
798        connectionError = true;
799    }
800    // methods to connect/disconnect to a source of data in an AbstractPortController
801
802    public AbstractPortController controller = null;
803
804    public boolean status() {
805        return (ostream != null && istream != null);
806    }
807
808    protected volatile Thread xmtThread = null;
809    protected volatile Thread rcvThread = null;
810
811    protected volatile Runnable xmtRunnable = null;
812
813    /**
814     * Make connection to an existing PortController object.
815     *
816     * @param p the PortController
817     */
818    public void connectPort(AbstractPortController p) {
819        rcvException = false;
820        connectionError = false;
821        xmtException = false;
822        threadStopRequest = false;
823        try {
824            istream = p.getInputStream();
825            ostream = p.getOutputStream();
826            if (controller != null) {
827                log.warn("connectPort: connect called while connected");
828            } else {
829                log.debug("connectPort invoked");
830            }
831            controller = p;
832            // and start threads
833            xmtThread = jmri.util.ThreadingUtil.newThread(
834                xmtRunnable = new Runnable() {
835                    @Override
836                    public void run() {
837                        try {
838                            transmitLoop();
839                        } catch (Throwable e) {
840                            if (!threadStopRequest) {
841                                log.error("Transmit thread terminated prematurely by: {}", e, e);
842                            }
843                            // see http://docs.oracle.com/javase/7/docs/api/java/lang/ThreadDeath.html
844                            // ThreadDeath must be thrown per Java API Javadocs
845                            //
846                            // The type ThreadDeath has been deprecated since version 20 and marked for removal
847                            // and the warning cannot be suppressed in Java 21. But external libraries might
848                            // throw the exception outside of JMRI control. So check the name of the exception
849                            // instead of using "instanceof".
850                            if ("java.lang.ThreadDeath".equals(e.getClass().getName())) {
851                                throw e;
852                            }
853                        }
854                }
855            });
856
857            String[] packages = this.getClass().getName().split("\\.");
858            xmtThread.setName(
859                (packages.length>=2 ? packages[packages.length-2]+"." :"")
860                +(packages.length>=1 ? packages[packages.length-1] :"")
861                +" Transmit thread");
862
863            xmtThread.setDaemon(true);
864            xmtThread.setPriority(Thread.MAX_PRIORITY-1);      //bump up the priority
865            xmtThread.start();
866
867            rcvThread = jmri.util.ThreadingUtil.newThread(
868                new Runnable() {
869                    @Override
870                    public void run() {
871                        receiveLoop();
872                    }
873                });
874            rcvThread.setName(
875                (packages.length>=2 ? packages[packages.length-2]+"." :"")
876                +(packages.length>=1 ? packages[packages.length-1] :"")
877                +" Receive thread");
878
879            rcvThread.setPriority(Thread.MAX_PRIORITY);      //bump up the priority
880            rcvThread.setDaemon(true);
881            rcvThread.start();
882
883        } catch (RuntimeException e) {
884            log.error("Failed to start up communications. Error was: ", e);
885            log.debug("Full trace:", e);
886        }
887    }
888
889    /**
890     * Get the port name for this connection from the TrafficController.
891     *
892     * @return the name of the port
893     */
894    public String getPortName() {
895        return controller.getCurrentPortName();
896    }
897
898    /**
899     * Break connection to existing PortController object. Once broken, attempts
900     * to send via "message" member will fail.
901     *
902     * @param p the PortController
903     */
904    public void disconnectPort(AbstractPortController p) {
905        istream = null;
906        ostream = null;
907        if (controller != p) {
908            log.warn("disconnectPort: disconnect called from non-connected AbstractPortController");
909        }
910        controller = null;
911        threadStopRequest = true;
912    }
913
914    /**
915     * Check if PortController object can be sent to.
916     *
917     * @param p the PortController
918     * @return true if ready, false otherwise May throw an Exception.
919     */
920    public boolean portReadyToSend(AbstractPortController p) {
921        if (p != null && !xmtException && !rcvException) {
922            return true;
923        } else {
924            return false;
925        }
926    }
927
928    // data members to hold the streams
929    protected DataInputStream istream = null;
930    protected OutputStream ostream = null;
931
932    protected boolean rcvException = false;
933
934    protected int maxRcvExceptionCount = 100;
935
936    /**
937     * Handle incoming characters. This is a permanent loop, looking for input
938     * messages in character form on the stream connected to the PortController
939     * via {@link #connectPort(AbstractPortController)}.
940     * <p>
941     * Each turn of the loop is the receipt of a single message.
942     */
943    public void receiveLoop() {
944        log.debug("receiveLoop starts in {}", this);
945        int errorCount = 0;
946        while (errorCount < maxRcvExceptionCount && !threadStopRequest) { // stream close will exit via exception
947            try {
948                handleOneIncomingReply();
949                errorCount = 0;
950            } catch (java.io.InterruptedIOException e) {
951                // related to InterruptedException, catch first
952                break;
953            } catch (IOException e) {
954                rcvException = true;
955                reportReceiveLoopException(e);
956                break;
957            } catch (RuntimeException e1) {
958                log.error("Exception in receive loop: {}", e1.toString(), e1);
959                errorCount++;
960                if (errorCount == maxRcvExceptionCount) {
961                    rcvException = true;
962                    reportReceiveLoopException(e1);
963                }
964            }
965        }
966        if (!threadStopRequest) { // if e.g. unexpected end
967            ConnectionStatus.instance().setConnectionState(controller.getSystemConnectionMemo(), ConnectionStatus.CONNECTION_DOWN);
968            log.debug("Exit from rcv loop in {}", this.getClass());
969            log.info("Exiting receive loop");
970            recovery(); // see if you can restart
971        }
972    }
973
974    /**
975     * Disconnect and reset the current PortController.
976     * Invoked at abnormal ending of receiveLoop.
977     */
978    protected final void recovery() {
979        AbstractPortController adapter = controller;
980        disconnectPort(controller);
981        adapter.recover();
982    }
983
984    /**
985     * Report an error on the receive loop. Separated so tests can suppress, even
986     * though message is asynchronous.
987     * @param e Exception encountered at lower level to trigger error, or null
988     */
989    protected void reportReceiveLoopException(Exception e) {
990        log.error("run: Exception: {} in {}", e.toString(), this.getClass().toString(), e);
991        jmri.jmrix.ConnectionStatus.instance().setConnectionState(controller.getSystemConnectionMemo(), jmri.jmrix.ConnectionStatus.CONNECTION_DOWN);
992        if (controller instanceof AbstractNetworkPortController) {
993            portWarnTCP(e);
994        }
995    }
996
997    protected abstract AbstractMRReply newReply();
998
999    protected abstract boolean endOfMessage(AbstractMRReply r);
1000
1001    /**
1002     * Dummy routine, to be filled by protocols that have to skip some
1003     * start-of-message characters.
1004     * @param istream input source
1005     * @throws IOException from underlying operations
1006     */
1007    protected void waitForStartOfReply(DataInputStream istream) throws IOException {
1008    }
1009
1010    /**
1011     * Read a single byte, protecting against various timeouts, etc.
1012     * <p>
1013     * When a port is set to have a receive timeout, some will return
1014     * zero bytes, an EOFException or a InterruptedIOException at the end of the timeout.
1015     * In that case, the read()
1016     * should be repeated to get the next real character.
1017     *
1018     * @param istream stream to read
1019     * @return the byte read
1020     * @throws java.io.IOException if unable to read
1021     */
1022    protected byte readByteProtected(DataInputStream istream) throws IOException {
1023        if (istream == null) {
1024            throw new IOException("Input Stream NULL when reading");
1025        }
1026        while (true) { // loop will repeat until character found
1027            int nchars;
1028            // The istream should be configured so that the following
1029            // read(..) call only blocks for a short time, e.g. 100msec, if no
1030            // data is available.  It's OK if it
1031            // throws e.g. java.io.InterruptedIOException
1032            // in that case, as the calling loop should just go around
1033            // and request input again.  This semi-blocking behavior will
1034            // let the terminateThreads() method end this thread cleanly.
1035            nchars = istream.read(rcvBuffer, 0, 1);
1036            if (nchars == -1) {
1037                // No more bytes can be read from the channel
1038                throw new IOException("Connection not terminated normally");
1039            }
1040            if (nchars > 0) {
1041                return rcvBuffer[0];
1042            }
1043        }
1044    }
1045
1046    // Defined this way to reduce new object creation
1047    private byte[] rcvBuffer = new byte[1];
1048
1049    /**
1050     * Get characters from the input source, and file a message.
1051     * <p>
1052     * Returns only when the message is complete.
1053     * <p>
1054     * Only used in the Receive thread.
1055     * <p>
1056     * Handles timeouts on read by ignoring zero-length reads.
1057     *
1058     * @param msg     message to fill
1059     * @param istream character source.
1060     * @throws IOException when presented by the input source.
1061     */
1062    protected void loadChars(AbstractMRReply msg, DataInputStream istream)
1063            throws IOException {
1064        int i;
1065        for (i = 0; i < msg.maxSize(); i++) {
1066            byte char1 = readByteProtected(istream);
1067            log.trace("char: {} i: {}",(char1&0xFF),i);
1068            // if there was a timeout, flush any char received and start over
1069            if (flushReceiveChars) {
1070                log.warn("timeout flushes receive buffer: {}", msg);
1071                msg.flush();
1072                i = 0;  // restart
1073                flushReceiveChars = false;
1074            }
1075            if (canReceive()) {
1076                msg.setElement(i, char1);
1077                if (endOfMessage(msg)) {
1078                    break;
1079                }
1080            } else {
1081                i--; // flush char
1082                log.error("unsolicited character received: {}", Integer.toHexString(char1));
1083            }
1084        }
1085    }
1086
1087    /**
1088     * Override in the system specific code if necessary
1089     *
1090     * @return true if it is okay to buffer receive characters into a reply
1091     *         message. When false, discard char received
1092     */
1093    protected boolean canReceive() {
1094        return true;
1095    }
1096
1097    private int retransmitCount = 0;
1098
1099    /**
1100     * Executes a reply distribution action on the appropriate thread for JMRI.
1101     * @param r a runnable typically encapsulating a MRReply and the iteration code needed to
1102     *          send it to all the listeners.
1103     */
1104    protected void distributeReply(Runnable r) {
1105        try {
1106            if (synchronizeRx) {
1107                SwingUtilities.invokeAndWait(r);
1108            } else {
1109                SwingUtilities.invokeLater(r);
1110            }
1111        } catch (InterruptedException ie) {
1112            if (threadStopRequest) return;
1113            log.error("Unexpected exception in invokeAndWait", ie);
1114        } catch (java.lang.reflect.InvocationTargetException| RuntimeException e) {
1115            // For InvocationTargetException, log the wrapped cause (the
1116            // real failure), not the reflection wrapper itself.
1117            Throwable toLog = (e instanceof java.lang.reflect.InvocationTargetException && e.getCause() != null)
1118                    ? e.getCause() : e;
1119            log.error("Unexpected exception in invokeAndWait", toLog);
1120            return;
1121        }
1122        log.debug("dispatch thread invoked");
1123    }
1124
1125    /**
1126     * Handle each reply when complete.
1127     * <p>
1128     * (This is public for testing purposes) Runs in the "Receive" thread.
1129     *
1130     * @throws java.io.IOException on error.
1131     */
1132    public void handleOneIncomingReply() throws IOException {
1133        // we sit in this until the message is complete, relying on
1134        // threading to let other stuff happen
1135
1136        // Create message off the right concrete class
1137        AbstractMRReply msg = newReply();
1138
1139        // wait for start if needed
1140        waitForStartOfReply(istream);
1141
1142        // message exists, now fill it
1143        loadChars(msg, istream);
1144
1145        if (threadStopRequest) return;
1146
1147        // message is complete, dispatch it !!
1148        replyInDispatch = true;
1149        log.debug("dispatch reply of length {} contains \"{}\", state {}", msg.getNumDataElements(), msg, mCurrentState);
1150
1151        // forward the message to the registered recipients,
1152        // which includes the communications monitor
1153        // return a notification via the Swing event queue to ensure proper thread
1154        Runnable r = new RcvNotifier(msg, mLastSender, this);
1155        distributeReply(r);
1156
1157        if (!msg.isUnsolicited()) {
1158            // effect on transmit:
1159            switch (mCurrentState) {
1160                case WAITMSGREPLYSTATE: {
1161                    // check to see if the response was an error message we want
1162                    // to automatically handle by re-queueing the last sent
1163                    // message, otherwise go on to the next message
1164                    if (msg.isRetransmittableErrorMsg()) {
1165                        log.error("Automatic Recovery from Error Message: {}.  Retransmitted {} times.", msg, retransmitCount);
1166                        synchronized (xmtRunnable) {
1167                            mCurrentState = AUTORETRYSTATE;
1168                            if (retransmitCount > 0) {
1169                                try {
1170                                    xmtRunnable.wait(retransmitCount * 100L);
1171                                } catch (InterruptedException e) {
1172                                    Thread.currentThread().interrupt(); // retain if needed later
1173                                }
1174                            }
1175                            replyInDispatch = false;
1176                            xmtRunnable.notify();
1177                            retransmitCount++;
1178                        }
1179                    } else {
1180                        // update state, and notify to continue
1181                        synchronized (xmtRunnable) {
1182                            mCurrentState = NOTIFIEDSTATE;
1183                            replyInDispatch = false;
1184                            xmtRunnable.notify();
1185                            retransmitCount = 0;
1186                        }
1187                    }
1188                    break;
1189                }
1190                case WAITREPLYINPROGMODESTATE: {
1191                    // entering programming mode
1192                    mCurrentMode = PROGRAMINGMODE;
1193                    replyInDispatch = false;
1194
1195                    // check to see if we need to delay to allow decoders to become
1196                    // responsive
1197                    int warmUpDelay = enterProgModeDelayTime();
1198                    if (warmUpDelay != 0) {
1199                        try {
1200                            synchronized (xmtRunnable) {
1201                                xmtRunnable.wait(warmUpDelay);
1202                            }
1203                        } catch (InterruptedException e) {
1204                            Thread.currentThread().interrupt(); // retain if needed later
1205                        }
1206                    }
1207                    // update state, and notify to continue
1208                    synchronized (xmtRunnable) {
1209                        mCurrentState = OKSENDMSGSTATE;
1210                        xmtRunnable.notify();
1211                    }
1212                    break;
1213                }
1214                case WAITREPLYINNORMMODESTATE: {
1215                    // entering normal mode
1216                    mCurrentMode = NORMALMODE;
1217                    replyInDispatch = false;
1218                    // update state, and notify to continue
1219                    synchronized (xmtRunnable) {
1220                        mCurrentState = OKSENDMSGSTATE;
1221                        xmtRunnable.notify();
1222                    }
1223                    break;
1224                }
1225                default: {
1226                    replyInDispatch = false;
1227                    if (allowUnexpectedReply) {
1228                        log.debug("Allowed unexpected reply received in state: {} was {}", mCurrentState, msg);
1229                        synchronized (xmtRunnable) {
1230                            // The transmit thread sometimes gets stuck
1231                            // when unexpected replies are received.  Notify
1232                            // it to clear the block without a timeout.
1233                            // (do not change the current state)
1234                            //if(mCurrentState!=IDLESTATE)
1235                            xmtRunnable.notify();
1236                        }
1237                    } else {
1238                        unexpectedReplyStateError(mCurrentState, msg.toString());
1239                    }
1240                }
1241            }
1242            // Unsolicited message
1243        } else {
1244            log.debug("Unsolicited Message Received {}", msg);
1245
1246            replyInDispatch = false;
1247        }
1248    }
1249
1250    /**
1251     * Log an error message for a message received in an unexpected state.
1252     * @param State message state.
1253     * @param msgString message string.
1254     */
1255    protected void unexpectedReplyStateError(int State, String msgString) {
1256       String[] packages = this.getClass().getName().split("\\.");
1257       String name = (packages.length>=2 ? packages[packages.length-2]+"." :"")
1258                     +(packages.length>=1 ? packages[packages.length-1] :"");
1259       log.error("reply complete in unexpected state: {} was {} in class {}", State, msgString, name);
1260    }
1261
1262    /**
1263     * for testing purposes, let us be able to find out
1264     * what the last sender was.
1265     * @return last sender, mLastSender.
1266     */
1267    public AbstractMRListener getLastSender() {
1268        return mLastSender;
1269    }
1270
1271    protected void terminate() {
1272        log.debug("Cleanup Starts");
1273        if (ostream == null) {
1274            return;    // no connection established
1275        }
1276        AbstractMRMessage modeMsg = enterNormalMode();
1277        if (modeMsg != null) {
1278            modeMsg.setRetries(100); // set the number of retries
1279            // high, just in case the interface
1280            // is busy when we try to send
1281            forwardToPort(modeMsg, null);
1282            // wait for reply
1283            try {
1284                if (xmtRunnable != null) {
1285                    synchronized (xmtRunnable) {
1286                        xmtRunnable.wait(modeMsg.getTimeout());
1287                    }
1288                }
1289            } catch (InterruptedException e) {
1290                Thread.currentThread().interrupt(); // retain if needed later
1291                log.error("transmit interrupted");
1292            }
1293        }
1294    }
1295
1296    /**
1297     * Internal class to remember the Reply object and destination listener with
1298     * a reply is received.
1299     */
1300    protected static class RcvNotifier implements Runnable {
1301
1302        AbstractMRReply mMsg;
1303        AbstractMRListener mDest;
1304        AbstractMRTrafficController mTc;
1305
1306        public RcvNotifier(AbstractMRReply pMsg, AbstractMRListener pDest,
1307                AbstractMRTrafficController pTc) {
1308            mMsg = pMsg;
1309            mDest = pDest;
1310            mTc = pTc;
1311        }
1312
1313        @Override
1314        public void run() {
1315            log.debug("Delayed rcv notify starts");
1316            mTc.notifyReply(mMsg, mDest);
1317        }
1318    } // end RcvNotifier
1319
1320    // allow creation of object outside package
1321    protected RcvNotifier newRcvNotifier(AbstractMRReply pMsg, AbstractMRListener pDest,
1322            AbstractMRTrafficController pTc) {
1323        return new RcvNotifier(pMsg, pDest, pTc);
1324    }
1325
1326    /**
1327     * Internal class to remember the Message object and destination listener
1328     * when a message is queued for notification.
1329     */
1330    protected static class XmtNotifier implements Runnable {
1331
1332        AbstractMRMessage mMsg;
1333        AbstractMRListener mDest;
1334        AbstractMRTrafficController mTc;
1335
1336        public XmtNotifier(AbstractMRMessage pMsg, AbstractMRListener pDest,
1337                AbstractMRTrafficController pTc) {
1338            mMsg = pMsg;
1339            mDest = pDest;
1340            mTc = pTc;
1341        }
1342
1343        @Override
1344        public void run() {
1345            log.debug("Delayed xmt notify starts");
1346            mTc.notifyMessage(mMsg, mDest);
1347        }
1348    }  // end XmtNotifier
1349
1350    /**
1351     * Terminate the receive and transmit threads.
1352     * <p>
1353     * This is intended to be used only by testing subclasses.
1354     */
1355    public void terminateThreads() {
1356        threadStopRequest = true;
1357        if (xmtThread != null) {
1358            xmtThread.interrupt();
1359            try {
1360                xmtThread.join(150);
1361            } catch (InterruptedException ie){
1362                // interrupted during cleanup.
1363            }
1364        }
1365
1366        if (rcvThread != null) {
1367            rcvThread.interrupt();
1368            try {
1369                rcvThread.join(150);
1370            } catch (InterruptedException ie){
1371                // interrupted during cleanup.
1372            }
1373        }
1374        // we also need to remove the shutdown task.
1375        InstanceManager.getDefault(ShutDownManager.class).deregister(shutDownTask);
1376    }
1377
1378    /**
1379     * Flag that threads should terminate as soon as they can.
1380     */
1381    protected volatile boolean threadStopRequest = false;
1382
1383    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(AbstractMRTrafficController.class);
1384
1385}