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    /**
160     * Break connection to an existing LnPortController object. Once broken,
161     * attempts to send via "message" member will fail.
162     *
163     * @param p previously connected port
164     */
165    public void disconnectPort(LnPortController p) {
166        istream = null;
167        ostream = null;
168        if (controller != p) {
169            log.warn("disconnectPort: disconnect called from non-connected LnPortController");
170        }
171        controller = null;
172    }
173
174    // data members to hold the streams. These are public so the inner classes defined here
175    // can access them with a Java 1.1 compiler
176    public DataInputStream istream = null;
177    public OutputStream ostream = null;
178
179    /**
180     * Read a single byte, protecting against various timeouts, etc.
181     * <p>
182     * When a port is set to have a receive timeout (via the
183     * enableReceiveTimeout() method), some will return zero bytes or an
184     * EOFException at the end of the timeout. In that case, the read should be
185     * repeated to get the next real character.
186     *
187     * @param istream stream to read from
188     * @return buffer of received data
189     * @throws java.io.IOException failure during stream read
190     *
191     */
192    protected byte readByteProtected(DataInputStream istream) throws java.io.IOException {
193        while (true) { // loop will repeat until character found
194            int nchars;
195            // The istream should be configured so that the following
196            // read(..) call only blocks for a short time, e.g. 100msec, if no
197            // data is available.  It's OK if it
198            // throws e.g. java.io.InterruptedIOException
199            // in that case, as the calling loop should just go around
200            // and request input again.  This semi-blocking behavior will
201            // let the terminateThreads() method end this thread cleanly.
202            nchars = istream.read(rcvBuffer, 0, 1);
203            if (nchars < 0) {
204                throw new EOFException(String.format("Stream read returned %d, indicating end-of-file", nchars));
205            }
206            if (nchars > 0) {
207                return rcvBuffer[0];
208            }
209        }
210    }
211    // Defined this way to reduce new object creation
212    private final byte[] rcvBuffer = new byte[1];
213
214    /**
215     * Captive class to handle incoming characters. This is a permanent loop,
216     * looking for input messages in character form on the stream connected to
217     * the LnPortController via <code>connectPort</code>.
218     */
219    protected class RcvHandler implements Runnable {
220
221        /**
222         * Remember the LnPacketizer object
223         */
224        LnTrafficController trafficController;
225
226        public RcvHandler(LnTrafficController lt) {
227            trafficController = lt;
228        }
229
230        /**
231         * Handle incoming characters. This is a permanent loop, looking for
232         * input messages in character form on the stream connected to the
233         * LnPortController via <code>connectPort</code>. Terminates with the
234         * input stream breaking out of the try block.
235         */
236        @Override
237        public void run() {
238
239            int opCode;
240            while (!threadStopRequest && ! Thread.interrupted() ) {   // loop until asked to stop
241                try {
242                    // start by looking for command -  skip if bit not set
243                    while (((opCode = (readByteProtected(istream) & 0xFF)) & 0x80) == 0) { // the real work is in the loop check
244                        log.trace("Skipping: {}", Integer.toHexString(opCode)); // NOI18N
245                    }
246                    // here opCode is OK. Create output message
247                    log.trace(" (RcvHandler) Start message with opcode: {}", Integer.toHexString(opCode)); // NOI18N
248                    LocoNetMessage msg = null;
249                    while (msg == null) {
250                        try {
251                            // Capture 2nd byte, always present
252                            int byte2 = readByteProtected(istream) & 0xFF;
253                            log.trace("Byte2: {}", Integer.toHexString(byte2)); // NOI18N
254                            int len = 2;
255                            switch ((opCode & 0x60) >> 5) {
256                                case 0:
257                                    /* 2 byte message */
258
259                                    len = 2;
260                                    break;
261
262                                case 1:
263                                    /* 4 byte message */
264
265                                    len = 4;
266                                    break;
267
268                                case 2:
269                                    /* 6 byte message */
270
271                                    len = 6;
272                                    break;
273
274                                case 3:
275                                    /* N byte message */
276
277                                    if (byte2 < 2) {
278                                        log.error("LocoNet message length invalid: {} opcode: {}", byte2, Integer.toHexString(opCode)); // NOI18N
279                                    }
280                                    len = byte2;
281                                    break;
282                                default:
283                                    log.warn("Unhandled code: {}", (opCode & 0x60) >> 5);
284                                    break;
285                            }
286                            msg = new LocoNetMessage(len);
287                            // message exists, now fill it
288                            msg.setOpCode(opCode);
289                            msg.setElement(1, byte2);
290                            log.trace("len: {}", len); // NOI18N
291                            for (int i = 2; i < len; i++) {
292                                // check for message-blocking error
293                                int b = readByteProtected(istream) & 0xFF;
294                                log.trace("char {} is: {}", i, Integer.toHexString(b)); // NOI18N
295                                if ((b & 0x80) != 0) {
296                                    log.warn("LocoNet message with opCode: {} ended early. Expected length: {} seen length: {} unexpected byte: {}", Integer.toHexString(opCode), len, i, Integer.toHexString(b)); // NOI18N
297                                    opCode = b;
298                                    throw new LocoNetMessageException();
299                                }
300                                msg.setElement(i, b);
301                            }
302                        } catch (LocoNetMessageException e) {
303                            // retry by destroying the existing message
304                            // opCode is set for the newly-started packet
305                            msg = null;
306                        }
307                    }
308                    // check parity
309                    if (!msg.checkParity()) {
310                        log.warn("Ignore LocoNet packet with bad checksum: {}", msg);
311                        throw new LocoNetMessageException();
312                    }
313                    // message is complete, dispatch it !!
314                    log.trace("message complete: {}", msg);
315
316                    // check if this message was supposed to be ignored
317                    // sentList will be empty if preference "LoconetUpdateSlotOnMessageCreation" is not activated
318                    if(trafficController.getSentList().contains(msg)) {
319                        trafficController.getSentList().remove(msg);
320                        log.trace("found packet {} in sentList, ignoring. {} packets in sentList remaining.", msg, trafficController.getSentList().size());
321                    }
322                    else {
323                        log.trace("queue message for notification: {}", msg);
324                        jmri.util.ThreadingUtil.runOnLayoutEventually(new RcvMemo(msg, trafficController));
325                    }
326
327                    // done with this one
328                } catch (LocoNetMessageException e) {
329                    // just let it ride for now
330                    log.warn("run: unexpected LocoNetMessageException", e); // NOI18N
331                } catch (java.io.InterruptedIOException e) {
332                    // posted from idle port when enableReceiveTimeout used
333                    // Normal condition, go around the loop again
334                } catch (java.io.IOException e) {
335                    // fired when read detects end-of-file
336                    log.info("End of file", e); // NOI18N
337                    dispose();
338                    disconnectPort(controller);
339                    return;
340                } catch (RuntimeException e) {
341                    // normally, we don't catch RuntimeException, but in this
342                    // permanently running loop it seems wise.
343                    log.warn("run: unexpected Exception", e); // NOI18N
344                }
345            } // end of permanent loop
346        }
347    }
348
349    /**
350     * Captive class to notify of one message.
351     */
352    protected static class RcvMemo implements jmri.util.ThreadingUtil.ThreadAction {
353
354        public RcvMemo(LocoNetMessage msg, LnTrafficController trafficController) {
355            thisMsg = msg;
356            thisTc = trafficController;
357        }
358        LocoNetMessage thisMsg;
359        LnTrafficController thisTc;
360
361        /**
362         * {@inheritDoc}
363         */
364        @Override
365        public void run() {
366            thisTc.notify(thisMsg);
367        }
368    }
369
370    /**
371     * Captive class to handle transmission.
372     */
373    class XmtHandler implements Runnable {
374
375        /**
376         * Loops forever, looking for message to send and processing them.
377         */
378        @Override
379        public void run() {
380
381            while (!threadStopRequest) {   // loop until asked to stop
382                // any input?
383                try {
384                    // get content; blocks until present
385                    log.trace("check for input"); // NOI18N
386
387                    byte[] msg = xmtList.take();
388
389                    // input - now send
390                    try {
391                        if (ostream != null) {
392                            if (isXmtBusy()) {
393                                log.debug("LocoNet port not ready to receive"); // NOI18N
394                            }
395                            log.trace("start write to stream: {}", jmri.util.StringUtil.hexStringFromBytes(msg)); // NOI18N
396                            ostream.write(msg);
397                            ostream.flush();
398                            log.trace("end write to stream: {}", jmri.util.StringUtil.hexStringFromBytes(msg)); // NOI18N
399                            messageTransmitted(msg);
400                        } else {
401                            // no stream connected
402                            log.warn("sendLocoNetMessage: no connection established"); // NOI18N
403                        }
404                    } catch (java.io.IOException e) {
405                        log.warn("sendLocoNetMessage: IOException: {}", e.toString()); // NOI18N
406                    }
407                } catch (InterruptedException ie) {
408                    return; // ending the thread
409                } catch (RuntimeException rt) {
410                    log.error("Exception on take() call", rt);
411                }
412            }
413        }
414    }
415
416    /**
417     * When a message is finally transmitted, forward it to listeners if echoing
418     * is needed.
419     *
420     * @param msg message sent
421     */
422    protected void messageTransmitted(byte[] msg) {
423        log.debug("message transmitted (echo {})", echo);
424        if (!echo) {
425            return;
426        }
427
428        LocoNetMessage m = new LocoNetMessage(msg);
429
430        // check if this message was supposed to be ignored
431        // sentList will be empty if preference "LoconetUpdateSlotOnMessageCreation" is not activated
432        if(getSentList().contains(m)) {
433            getSentList().remove(m);
434            log.trace("found packet {} in sentList, ignoring. {} packets in sentList remaining.", m, getSentList().size());
435        }
436        else {
437            log.trace("queue message for notification: {}", m);
438            // message is queued for transmit, echo it when needed
439            // return a notification via the queue to ensure end
440            javax.swing.SwingUtilities.invokeLater(new Echo(this, m));
441        }
442    }
443
444    static class Echo implements Runnable {
445
446        Echo(LnPacketizer t, LocoNetMessage m) {
447            myTc = t;
448            msgForLater = m;
449        }
450        LocoNetMessage msgForLater;
451        LnPacketizer myTc;
452
453        /**
454         * {@inheritDoc}
455         */
456        @Override
457        public void run() {
458            myTc.notify(msgForLater);
459        }
460    }
461
462    /**
463     * Invoked at startup to start the threads needed here.
464     */
465    public void startThreads() {
466        int priority = Thread.currentThread().getPriority();
467        log.debug("startThreads current priority = {} max available = {} default = {} min available = {}", // NOI18N
468                priority, Thread.MAX_PRIORITY, Thread.NORM_PRIORITY, Thread.MIN_PRIORITY);
469
470        // start the RcvHandler in a thread of its own
471        if (rcvHandler == null) {
472            rcvHandler = new RcvHandler(this);
473        }
474        rcvThread = jmri.util.ThreadingUtil.newThread(rcvHandler, "LocoNet receive handler"); // NOI18N
475        rcvThread.setDaemon(true);
476        rcvThread.setPriority(Thread.MAX_PRIORITY);
477        rcvThread.start();
478
479        if (xmtHandler == null) {
480            xmtHandler = new XmtHandler();
481        }
482        // make sure that the xmt priority is no lower than the current priority
483        int xmtpriority = (Thread.MAX_PRIORITY - 1 > priority ? Thread.MAX_PRIORITY - 1 : Thread.MAX_PRIORITY);
484        // start the XmtHandler in a thread of its own
485        if (xmtThread == null) {
486            xmtThread = jmri.util.ThreadingUtil.newThread(xmtHandler, "LocoNet transmit handler"); // NOI18N
487        }
488        log.debug("Xmt thread starts at priority {}", xmtpriority); // NOI18N
489        xmtThread.setDaemon(true);
490        xmtThread.setPriority(Thread.MAX_PRIORITY - 1);
491        xmtThread.start();
492
493        log.info("lnPacketizer Started");
494    }
495
496    protected Thread rcvThread;
497    protected Thread xmtThread;
498
499    /**
500     * {@inheritDoc}
501     */
502    // The join(150) is using a timeout because some receive threads
503    // (and maybe some day transmit threads) use calls that block
504    // even when interrupted.  We wait 150 msec and proceed.
505    // Threads that do that are responsible for ending cleanly
506    // when the blocked call eventually returns.
507    @Override
508    public void dispose() {
509        threadStopRequest = true;
510        if (xmtThread != null) {
511            xmtThread.interrupt();
512            try {
513                xmtThread.join(150);
514            } catch (InterruptedException e) { log.warn("unexpected InterruptedException", e);}
515        }
516        if (rcvThread != null) {
517            rcvThread.interrupt();
518            try {
519                rcvThread.join(150);
520            } catch (InterruptedException e) { log.warn("unexpected InterruptedException", e);}
521        }
522        super.dispose();
523    }
524
525    /**
526     * Terminate the receive and transmit threads.
527     * <p>
528     * This is intended to be used only by testing subclasses.
529     */
530    // The join(150) is using a timeout because some receive threads
531    // (and maybe some day transmit threads) use calls that block
532    // even when interrupted.  We wait 150 msec and proceed.
533    // Threads that do that are responsible for ending cleanly
534    // when the blocked call eventually returns.
535    public void terminateThreads() {
536        threadStopRequest = true;
537        if (xmtThread != null) {
538            xmtThread.interrupt();
539            try {
540                xmtThread.join(150);
541            } catch (InterruptedException ie){
542                // interrupted during cleanup.
543            }
544        }
545
546        if (rcvThread != null) {
547            rcvThread.interrupt();
548            try {
549                rcvThread.join(150);
550            } catch (InterruptedException ie){
551                // interrupted during cleanup.
552            }
553        }
554    }
555
556    /**
557     * Flag that threads should terminate as soon as they can.
558     */
559    protected volatile boolean threadStopRequest = false;
560
561    private static final Logger log = LoggerFactory.getLogger(LnPacketizer.class);
562
563}