001package jmri.jmrix.loconet;
002
003import java.io.DataInputStream;
004import java.io.EOFException;
005import java.io.OutputStream;
006import java.util.concurrent.LinkedTransferQueue;
007import org.slf4j.Logger;
008import org.slf4j.LoggerFactory;
009
010/**
011 * Converts Stream-based I/O to/from LocoNet messages. The "LocoNetInterface"
012 * side sends/receives LocoNetMessage objects. The connection to a
013 * LnPortController is via a pair of *Streams, which then carry sequences of
014 * characters for transmission.
015 * <p>
016 * Messages come to this via the main GUI thread, and are forwarded back to
017 * listeners in that same thread. Reception and transmission are handled in
018 * dedicated threads by RcvHandler and XmtHandler objects. Those are internal
019 * classes defined here. The thread priorities are:
020 * <ul>
021 *   <li> RcvHandler - at highest available priority
022 *   <li> XmtHandler - down one, which is assumed to be above the GUI
023 *   <li> (everything else)
024 * </ul>
025 * Some of the message formats used in this class are Copyright Digitrax, Inc.
026 * and used with permission as part of the JMRI project. That permission does
027 * not extend to uses in other software products. If you wish to use this code,
028 * algorithm or these message formats outside of JMRI, please contact Digitrax
029 * Inc for separate permission.
030 *
031 * @author Bob Jacobsen Copyright (C) 2001, 2018
032 * @author B. Milhaupt  Copyright (C) 2020
033 */
034public class LnPacketizer extends LnTrafficController {
035
036    /**
037     * True if the external hardware is not echoing messages, so we must.
038     */
039    protected boolean echo = false;  // true = echo messages here, instead of in hardware
040
041    public LnPacketizer(LocoNetSystemConnectionMemo m) {
042        // set the memo to point here
043        memo = m;
044        m.setLnTrafficController(this);
045    }
046
047    // The methods to implement the LocoNetInterface
048
049    /**
050     * {@inheritDoc}
051     */
052    @Override
053    public boolean status() {
054        boolean returnVal = ( ostream != null && istream != null
055                && xmtThread != null && xmtThread.isAlive() && xmtHandler != null
056                && rcvThread != null && rcvThread.isAlive() && rcvHandler != null
057                );
058        return returnVal;
059    }
060
061    /**
062     * Synchronized list used as a transmit queue.
063     */
064    protected LinkedTransferQueue<byte[]> xmtList = new LinkedTransferQueue<>();
065
066    /**
067     * XmtHandler (a local class) object to implement the transmit thread.
068     * <p>
069     * We create this object in startThreads() as each packetizer uses different handlers.
070     * So long as the object is created before using it to sync it works.
071     *
072     */
073    protected Runnable xmtHandler = null;
074
075    /**
076     * RcvHandler (a local class) object to implement the receive thread
077     */
078    protected Runnable rcvHandler;
079
080    /**
081     * Forward a preformatted LocoNetMessage to the actual interface.
082     * <p>
083     * Checksum is computed and overwritten here, then the message is converted
084     * to a byte array and queued for transmission.
085     *
086     * @param m  Message to send; will be updated with CRC
087     * @param requestIgnoreEcho  If true: Notify listeners on enqueing message, ignore echo from line.
088     *                           Only in effect if preference "LoconetUpdateSlotOnMessageCreation" is set.
089     */
090    @Override
091    public void sendLocoNetMessage(LocoNetMessage m, boolean requestIgnoreEcho) {
092
093        // update statistics
094        transmittedMsgCount++;
095
096        // set the error correcting code byte(s) before transmittal
097        m.setParity();
098
099        // stream to port in single write, as that's needed by serial
100        int len = m.getNumDataElements();
101        byte[] msg = new byte[len];
102        for (int i = 0; i < len; i++) {
103            msg[i] = (byte) m.getElement(i);
104        }
105
106        log.debug("queue LocoNet packet: {}", m);
107        // We need to queue the request and wake the xmit thread in an atomic operation
108        // But the thread might not be running, in which case the request is just
109        // queued up.
110        try {
111            xmtList.add(msg);
112            // save to queue if we want to remember it to check in receive handler
113            if (mLoconetUpdateSlotOnMessageCreation && requestIgnoreEcho) {
114                log.trace("add LocoNet packet {} to sentList. Now {} packets in sentList.", m, sentList.size());
115                sentList.add(m);
116                log.trace("queue message for notification: {}", m);
117                jmri.util.ThreadingUtil.runOnLayoutEventually(new RcvMemo(m, this));
118            }
119        } catch (RuntimeException e) {
120            log.warn("passing to xmit: unexpected exception: ", e);
121        }
122
123    }
124
125    /**
126     * Implement abstract method to signal if there's a backlog of information
127     * waiting to be sent.
128     *
129     * @return true if busy, false if nothing waiting to send
130     */
131    @Override
132    public boolean isXmtBusy() {
133        if (controller == null) {
134            return false;
135        }
136
137        return (!controller.okToSend());
138    }
139
140    // methods to connect/disconnect to a source of data in a LnPortController
141
142    protected LnPortController controller = null;
143
144    /**
145     * Make connection to an existing LnPortController object.
146     *
147     * @param p Port controller for connected. Save this for a later disconnect
148     *          call
149     */
150    public void connectPort(LnPortController p) {
151        istream = p.getInputStream();
152        ostream = p.getOutputStream();
153        if (controller != null) {
154            log.warn("connectPort: connect called while connected");
155        }
156        controller = p;
157    }
158
159    /** Starts a new receive thread after a reconnect. */
160    public void restartRcvThread() {
161        rcvThread = jmri.util.ThreadingUtil.newThread(rcvHandler, "LocoNet receive handler"); // NOI18N
162        rcvThread.setDaemon(true);
163        rcvThread.setPriority(Thread.MAX_PRIORITY);
164        rcvThread.start();
165    }
166
167    /**
168     * Break connection to an existing LnPortController object. Once broken,
169     * attempts to send via "message" member will fail.
170     *
171     * @param p previously connected port
172     */
173    public void disconnectPort(LnPortController p) {
174        istream = null;
175        ostream = null;
176        if (controller != p) {
177            log.warn("disconnectPort: disconnect called from non-connected LnPortController");
178        }
179        controller = null;
180    }
181
182    // data members to hold the streams. These are public so the inner classes defined here
183    // can access them with a Java 1.1 compiler
184    public DataInputStream istream = null;
185    public OutputStream ostream = null;
186
187    /**
188     * Read a single byte, protecting against various timeouts, etc.
189     * <p>
190     * When a port is set to have a receive timeout (via the
191     * enableReceiveTimeout() method), some will return zero bytes or an
192     * EOFException at the end of the timeout. In that case, the read should be
193     * repeated to get the next real character.
194     *
195     * @param istream stream to read from
196     * @return buffer of received data
197     * @throws java.io.IOException failure during stream read
198     *
199     */
200    protected byte readByteProtected(DataInputStream istream) throws java.io.IOException {
201        while (true) { // loop will repeat until character found
202            int nchars;
203            // The istream should be configured so that the following
204            // read(..) call only blocks for a short time, e.g. 100msec, if no
205            // data is available.  It's OK if it
206            // throws e.g. java.io.InterruptedIOException
207            // in that case, as the calling loop should just go around
208            // and request input again.  This semi-blocking behavior will
209            // let the terminateThreads() method end this thread cleanly.
210            nchars = istream.read(rcvBuffer, 0, 1);
211            if (nchars < 0) {
212                throw new EOFException(String.format("Stream read returned %d, indicating end-of-file", nchars));
213            }
214            if (nchars > 0) {
215                return rcvBuffer[0];
216            }
217        }
218    }
219    // Defined this way to reduce new object creation
220    private final byte[] rcvBuffer = new byte[1];
221
222    /**
223     * Captive class to handle incoming characters. This is a permanent loop,
224     * looking for input messages in character form on the stream connected to
225     * the LnPortController via <code>connectPort</code>.
226     */
227    protected class RcvHandler implements Runnable {
228
229        /**
230         * Remember the LnPacketizer object
231         */
232        LnTrafficController trafficController;
233
234        public RcvHandler(LnTrafficController lt) {
235            trafficController = lt;
236        }
237
238        /**
239         * Handle incoming characters. This is a permanent loop, looking for
240         * input messages in character form on the stream connected to the
241         * LnPortController via <code>connectPort</code>. Terminates with the
242         * input stream breaking out of the try block.
243         */
244        @Override
245        public void run() {
246
247            int opCode;
248            while (!threadStopRequest && ! Thread.interrupted() ) {   // loop until asked to stop
249                try {
250                    // start by looking for command -  skip if bit not set
251                    while (((opCode = (readByteProtected(istream) & 0xFF)) & 0x80) == 0) { // the real work is in the loop check
252                        log.trace("Skipping: {}", Integer.toHexString(opCode)); // NOI18N
253                    }
254                    // here opCode is OK. Create output message
255                    log.trace(" (RcvHandler) Start message with opcode: {}", Integer.toHexString(opCode)); // NOI18N
256                    LocoNetMessage msg = null;
257                    while (msg == null) {
258                        try {
259                            // Capture 2nd byte, always present
260                            int byte2 = readByteProtected(istream) & 0xFF;
261                            log.trace("Byte2: {}", Integer.toHexString(byte2)); // NOI18N
262                            int len = 2;
263                            switch ((opCode & 0x60) >> 5) {
264                                case 0:
265                                    /* 2 byte message */
266
267                                    len = 2;
268                                    break;
269
270                                case 1:
271                                    /* 4 byte message */
272
273                                    len = 4;
274                                    break;
275
276                                case 2:
277                                    /* 6 byte message */
278
279                                    len = 6;
280                                    break;
281
282                                case 3:
283                                    /* N byte message */
284
285                                    if (byte2 < 2) {
286                                        log.error("LocoNet message length invalid: {} opcode: {}", byte2, Integer.toHexString(opCode)); // NOI18N
287                                    }
288                                    len = byte2;
289                                    break;
290                                default:
291                                    log.warn("Unhandled code: {}", (opCode & 0x60) >> 5);
292                                    break;
293                            }
294                            msg = new LocoNetMessage(len);
295                            // message exists, now fill it
296                            msg.setOpCode(opCode);
297                            msg.setElement(1, byte2);
298                            log.trace("len: {}", len); // NOI18N
299                            for (int i = 2; i < len; i++) {
300                                // check for message-blocking error
301                                int b = readByteProtected(istream) & 0xFF;
302                                log.trace("char {} is: {}", i, Integer.toHexString(b)); // NOI18N
303                                if ((b & 0x80) != 0) {
304                                    log.warn("LocoNet message with opCode: {} ended early. Expected length: {} seen length: {} unexpected byte: {}", Integer.toHexString(opCode), len, i, Integer.toHexString(b)); // NOI18N
305                                    opCode = b;
306                                    throw new LocoNetMessageException();
307                                }
308                                msg.setElement(i, b);
309                            }
310                        } catch (LocoNetMessageException e) {
311                            // retry by destroying the existing message
312                            // opCode is set for the newly-started packet
313                            msg = null;
314                        }
315                    }
316                    // check parity
317                    if (!msg.checkParity()) {
318                        log.warn("Ignore LocoNet packet with bad checksum: {}", msg);
319                        throw new LocoNetMessageException();
320                    }
321                    // message is complete, dispatch it !!
322                    log.trace("message complete: {}", msg);
323
324                    // check if this message was supposed to be ignored
325                    // sentList will be empty if preference "LoconetUpdateSlotOnMessageCreation" is not activated
326                    if(trafficController.getSentList().contains(msg)) {
327                        trafficController.getSentList().remove(msg);
328                        log.trace("found packet {} in sentList, ignoring. {} packets in sentList remaining.", msg, trafficController.getSentList().size());
329                    }
330                    else {
331                        log.trace("queue message for notification: {}", msg);
332                        jmri.util.ThreadingUtil.runOnLayoutEventually(new RcvMemo(msg, trafficController));
333                    }
334
335                    // done with this one
336                } catch (LocoNetMessageException e) {
337                    // just let it ride for now
338                    log.warn("run: unexpected LocoNetMessageException", e); // NOI18N
339                } catch (java.io.InterruptedIOException e) {
340                    // posted from idle port when enableReceiveTimeout used
341                    // Normal condition, go around the loop again
342                } catch (java.io.IOException e) {
343                    if (controller.getAllowConnectionRecovery()) {
344                        log.info("run: server closed connection, attempting recovery");
345                        controller.closePort();
346                        controller.recover();
347                    } else {
348                        // fired when read detects end-of-file
349                        log.info("End of file", e); // NOI18N
350                        dispose();
351                        disconnectPort(controller);
352                    }
353                    return;
354                } catch (RuntimeException e) {
355                    // normally, we don't catch RuntimeException, but in this
356                    // permanently running loop it seems wise.
357                    log.warn("run: unexpected Exception", e); // NOI18N
358                }
359            } // end of permanent loop
360        }
361    }
362
363    /**
364     * Captive class to notify of one message.
365     */
366    protected static class RcvMemo implements jmri.util.ThreadingUtil.ThreadAction {
367
368        public RcvMemo(LocoNetMessage msg, LnTrafficController trafficController) {
369            thisMsg = msg;
370            thisTc = trafficController;
371        }
372        LocoNetMessage thisMsg;
373        LnTrafficController thisTc;
374
375        /**
376         * {@inheritDoc}
377         */
378        @Override
379        public void run() {
380            thisTc.notify(thisMsg);
381        }
382    }
383
384    /**
385     * Captive class to handle transmission.
386     */
387    class XmtHandler implements Runnable {
388
389        /**
390         * Loops forever, looking for message to send and processing them.
391         */
392        @Override
393        public void run() {
394
395            while (!threadStopRequest) {   // loop until asked to stop
396                // any input?
397                try {
398                    // get content; blocks until present
399                    log.trace("check for input"); // NOI18N
400
401                    byte[] msg = xmtList.take();
402
403                    // input - now send
404                    try {
405                        if (ostream != null) {
406                            if (isXmtBusy()) {
407                                log.debug("LocoNet port not ready to receive"); // NOI18N
408                            }
409                            log.trace("start write to stream: {}", jmri.util.StringUtil.hexStringFromBytes(msg)); // NOI18N
410                            ostream.write(msg);
411                            ostream.flush();
412                            log.trace("end write to stream: {}", jmri.util.StringUtil.hexStringFromBytes(msg)); // NOI18N
413                            messageTransmitted(msg);
414                        } else {
415                            // no stream connected
416                            log.warn("sendLocoNetMessage: no connection established"); // NOI18N
417                        }
418                    } catch (java.io.IOException e) {
419                        log.warn("sendLocoNetMessage: IOException: {}", e.toString()); // NOI18N
420
421                        if (controller.getAllowConnectionRecovery()) {
422                            log.info("run: server closed connection, attempting recovery");
423                            controller.closePort();
424                            controller.recover();
425                        }
426                    }
427                } catch (InterruptedException ie) {
428                    return; // ending the thread
429                } catch (RuntimeException rt) {
430                    log.error("Exception on take() call", rt);
431                }
432            }
433        }
434    }
435
436    /**
437     * When a message is finally transmitted, forward it to listeners if echoing
438     * is needed.
439     *
440     * @param msg message sent
441     */
442    protected void messageTransmitted(byte[] msg) {
443        log.debug("message transmitted (echo {})", echo);
444        if (!echo) {
445            return;
446        }
447
448        LocoNetMessage m = new LocoNetMessage(msg);
449
450        // check if this message was supposed to be ignored
451        // sentList will be empty if preference "LoconetUpdateSlotOnMessageCreation" is not activated
452        if(getSentList().contains(m)) {
453            getSentList().remove(m);
454            log.trace("found packet {} in sentList, ignoring. {} packets in sentList remaining.", m, getSentList().size());
455        }
456        else {
457            log.trace("queue message for notification: {}", m);
458            // message is queued for transmit, echo it when needed
459            // return a notification via the queue to ensure end
460            javax.swing.SwingUtilities.invokeLater(new Echo(this, m));
461        }
462    }
463
464    static class Echo implements Runnable {
465
466        Echo(LnPacketizer t, LocoNetMessage m) {
467            myTc = t;
468            msgForLater = m;
469        }
470        LocoNetMessage msgForLater;
471        LnPacketizer myTc;
472
473        /**
474         * {@inheritDoc}
475         */
476        @Override
477        public void run() {
478            myTc.notify(msgForLater);
479        }
480    }
481
482    /**
483     * Invoked at startup to start the threads needed here.
484     */
485    public void startThreads() {
486        int priority = Thread.currentThread().getPriority();
487        log.debug("startThreads current priority = {} max available = {} default = {} min available = {}", // NOI18N
488                priority, Thread.MAX_PRIORITY, Thread.NORM_PRIORITY, Thread.MIN_PRIORITY);
489
490        // start the RcvHandler in a thread of its own
491        if (rcvHandler == null) {
492            rcvHandler = new RcvHandler(this);
493        }
494        rcvThread = jmri.util.ThreadingUtil.newThread(rcvHandler, "LocoNet receive handler"); // NOI18N
495        rcvThread.setDaemon(true);
496        rcvThread.setPriority(Thread.MAX_PRIORITY);
497        rcvThread.start();
498
499        if (xmtHandler == null) {
500            xmtHandler = new XmtHandler();
501        }
502        // make sure that the xmt priority is no lower than the current priority
503        int xmtpriority = (Thread.MAX_PRIORITY - 1 > priority ? Thread.MAX_PRIORITY - 1 : Thread.MAX_PRIORITY);
504        // start the XmtHandler in a thread of its own
505        if (xmtThread == null) {
506            xmtThread = jmri.util.ThreadingUtil.newThread(xmtHandler, "LocoNet transmit handler"); // NOI18N
507        }
508        log.debug("Xmt thread starts at priority {}", xmtpriority); // NOI18N
509        xmtThread.setDaemon(true);
510        xmtThread.setPriority(Thread.MAX_PRIORITY - 1);
511        xmtThread.start();
512
513        log.info("lnPacketizer Started");
514    }
515
516    protected Thread rcvThread;
517    protected Thread xmtThread;
518
519    /**
520     * {@inheritDoc}
521     */
522    // The join(150) is using a timeout because some receive threads
523    // (and maybe some day transmit threads) use calls that block
524    // even when interrupted.  We wait 150 msec and proceed.
525    // Threads that do that are responsible for ending cleanly
526    // when the blocked call eventually returns.
527    @Override
528    public void dispose() {
529        threadStopRequest = true;
530        if (xmtThread != null) {
531            xmtThread.interrupt();
532            try {
533                xmtThread.join(150);
534            } catch (InterruptedException e) { log.warn("unexpected InterruptedException", e);}
535        }
536        if (rcvThread != null) {
537            rcvThread.interrupt();
538            try {
539                rcvThread.join(150);
540            } catch (InterruptedException e) { log.warn("unexpected InterruptedException", e);}
541        }
542        super.dispose();
543    }
544
545    /**
546     * Terminate the receive and transmit threads.
547     * <p>
548     * This is intended to be used only by testing subclasses.
549     */
550    // The join(150) is using a timeout because some receive threads
551    // (and maybe some day transmit threads) use calls that block
552    // even when interrupted.  We wait 150 msec and proceed.
553    // Threads that do that are responsible for ending cleanly
554    // when the blocked call eventually returns.
555    public void terminateThreads() {
556        threadStopRequest = true;
557        if (xmtThread != null) {
558            xmtThread.interrupt();
559            try {
560                xmtThread.join(150);
561            } catch (InterruptedException ie){
562                // interrupted during cleanup.
563            }
564        }
565
566        if (rcvThread != null) {
567            rcvThread.interrupt();
568            try {
569                rcvThread.join(150);
570            } catch (InterruptedException ie){
571                // interrupted during cleanup.
572            }
573        }
574    }
575
576    /**
577     * Flag that threads should terminate as soon as they can.
578     */
579    protected volatile boolean threadStopRequest = false;
580
581    private static final Logger log = LoggerFactory.getLogger(LnPacketizer.class);
582
583}