001package jmri.jmrix.loconet.uhlenbrock;
002
003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
004import java.util.Calendar;
005import java.util.concurrent.ConcurrentLinkedQueue;
006import jmri.jmrix.loconet.LnPacketizer;
007import jmri.jmrix.loconet.LocoNetMessage;
008import jmri.jmrix.loconet.LocoNetMessageException;
009import jmri.jmrix.loconet.LocoNetSystemConnectionMemo;
010import org.slf4j.Logger;
011import org.slf4j.LoggerFactory;
012
013/**
014 * Converts Stream-based I/O to/from LocoNet messages. The "LocoNetInterface"
015 * side sends/receives LocoNetMessage objects. The connection to a
016 * LnPortController is via a pair of *Streams, which then carry sequences of
017 * characters for transmission.
018 * <p>
019 * Messages come to this via the main GUI thread, and are forwarded back to
020 * listeners in that same thread. Reception and transmission are handled in
021 * dedicated threads by RcvHandler and XmtHandler objects. Those are internal
022 * classes defined here. The thread priorities are:
023 * <ul>
024 *   <li> RcvHandler - at highest available priority
025 *   <li> XmtHandler - down one, which is assumed to be above the GUI
026 *   <li> (everything else)
027 * </ul>
028 *
029 * Some of the message formats used in this class are Copyright Digitrax, Inc.
030 * and used with permission as part of the JMRI project. That permission does
031 * not extend to uses in other software products. If you wish to use this code,
032 * algorithm or these message formats outside of JMRI, please contact Digitrax
033 * Inc for separate permission.
034 *
035 * @author Bob Jacobsen Copyright (C) 2001, 2010
036 */
037public class UhlenbrockPacketizer extends LnPacketizer {
038
039    @SuppressFBWarnings(value = "ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD",
040            justification = "Only used during system initialization")
041    public UhlenbrockPacketizer(LocoNetSystemConnectionMemo m) {
042        super(m);
043        log.debug("UhlenbrockPacketizer instantiated");
044    }
045
046    public static final int NOTIFIEDSTATE = 15;    // xmt notified, will next wake
047    public static final int WAITMSGREPLYSTATE = 25;  // xmt has sent, await reply to message
048
049    static int defaultWaitTimer = 10000;
050
051    /**
052     * Forward a preformatted LocoNetMessage to the actual interface.
053     *
054     * Checksum is computed and overwritten here, then the message is converted
055     * to a byte array and queued for transmission.
056     *
057     * @param m  Message to send; will be updated with CRC
058     * @param requestIgnoreEcho  If true: Notify listeners on enqueing message, ignore echo from line.
059     *                           Only in effect if preference "LoconetUpdateSlotOnMessageCreation" is set.
060     */
061    @Override
062    public void sendLocoNetMessage(LocoNetMessage m, boolean requestIgnoreEcho) {
063        log.debug("add to queue message {}", m.toString());
064        // update statistics
065        transmittedMsgCount++;
066
067        // set the error correcting code byte(s) before transmittal
068        m.setParity();
069
070        // stream to port in single write, as that's needed by serial
071        int len = m.getNumDataElements();
072        byte msg[] = new byte[len];
073        for (int i = 0; i < len; i++) {
074            msg[i] = (byte) m.getElement(i);
075        }
076
077        log.trace("queue LocoNet packet: {}", m.toString());
078        // queue the request
079        try {
080            xmtLocoNetList.add(m); // done first to make sure it's there before xmtList has an element
081            xmtList.add(msg);
082            // save to queue if we want to remember it to check in receive handler
083            if (mLoconetUpdateSlotOnMessageCreation && requestIgnoreEcho) {
084                log.trace("add LocoNet packet {} to sentList. Now {} packets in sentList.", m, sentList.size());
085                sentList.add(m);
086                log.trace("queue message for notification: {}", m);
087                jmri.util.ThreadingUtil.runOnLayoutEventually(new RcvMemo(m, this));
088            }
089        } catch (RuntimeException e) {
090            log.warn("passing to xmit: unexpected exception: ", e);
091        }
092    }
093
094    /**
095     * Synchronized list used as a transmit queue.
096     * <p>
097     * This is public to allow access from the internal class(es) when compiling
098     * with Java 1.1
099     */
100    public ConcurrentLinkedQueue<LocoNetMessage> xmtLocoNetList = new ConcurrentLinkedQueue<>();
101
102    /**
103     * Captive class to handle incoming characters. This is a permanent loop,
104     * looking for input messages in character form on the stream connected to
105     * the LnPortController via <code>connectPort</code>.
106     */
107    class RcvHandler implements Runnable {
108
109        /**
110         * Remember the LnPacketizer object.
111         */
112        LnPacketizer trafficController;
113
114        public RcvHandler(LnPacketizer lt) {
115            trafficController = lt;
116        }
117
118        @Override
119        public void run() {
120
121            int opCode;
122            while (true) {   // loop permanently, program close will exit
123                try {
124                    // start by looking for command -  skip if bit not set
125                    int inbyte = readByteProtected(istream) & 0xFF;
126                    while (((opCode = (inbyte)) & 0x80) == 0) {
127                        log.debug("Skipping: {}", Integer.toHexString(opCode));
128                        inbyte = readByteProtected(istream) & 0xFF;
129                    }
130                    // here opCode is OK. Create output message
131                    log.debug("Start message with opcode: {}", Integer.toHexString(opCode));
132                    LocoNetMessage msg = null;
133                    while (msg == null) {
134                        try {
135                            // Capture 2nd byte, always present
136                            int byte2 = readByteProtected(istream) & 0xFF;
137                            //log.debug("Byte2: "+Integer.toHexString(byte2));
138                            if ((byte2 & 0x80) != 0) {
139                                log.warn("LocoNet message with opCode: {} ended early. Byte2 is also an opcode: {}", Integer.toHexString(opCode), Integer.toHexString(byte2));
140                                opCode = byte2;
141                                throw new LocoNetMessageException();
142                            }
143
144                            // Decide length
145                            switch ((opCode & 0x60) >> 5) {
146                                case 0:
147                                    /* 2 byte message */
148
149                                    msg = new LocoNetMessage(2);
150                                    break;
151
152                                case 1:
153                                    /* 4 byte message */
154
155                                    msg = new LocoNetMessage(4);
156                                    break;
157
158                                case 2:
159                                    /* 6 byte message */
160
161                                    msg = new LocoNetMessage(6);
162                                    break;
163
164                                case 3:
165                                    /* N byte message */
166
167                                    if (byte2 < 2) {
168                                        log.error("LocoNet message length invalid: {} opcode: {}", byte2, Integer.toHexString(opCode));
169                                    }
170                                    msg = new LocoNetMessage(byte2);
171                                    break;
172                                default: // can't happen with this code, but just in case...
173                                    throw new LocoNetMessageException("decode failure " + byte2);
174                            }
175                            // message exists, now fill it
176                            msg.setOpCode(opCode);
177                            msg.setElement(1, byte2);
178                            int len = msg.getNumDataElements();
179                            //log.debug("len: "+len);
180                            for (int i = 2; i < len; i++) {
181                                // check for message-blocking error
182                                int b = readByteProtected(istream) & 0xFF;
183                                //log.debug("char "+i+" is: "+Integer.toHexString(b));
184                                if ((b & 0x80) != 0) {
185                                    log.warn("LocoNet message with opCode: {} ended early. Expected length: {} seen length: {} unexpected byte: {}", Integer.toHexString(opCode), len, i, Integer.toHexString(b));
186                                    opCode = b;
187                                    throw new LocoNetMessageException();
188                                }
189                                msg.setElement(i, b);
190                            }
191                        } catch (LocoNetMessageException e) {
192                            // retry by going around again
193                            // opCode is set for the newly-started packet
194                            msg = null;
195                            continue;
196                        }
197                    }
198                    // check parity
199                    if (!msg.checkParity()) {
200                        log.warn("Ignore LocoNet packet with bad checksum: {}", msg.toString());
201                        throw new LocoNetMessageException();
202                    }
203
204                    synchronized (xmtHandler) {
205                        if (mCurrentState == WAITMSGREPLYSTATE && msg.equals(lastMessage)) {
206                            log.debug("We have our returned message and can send back out our next instruction");
207                            mCurrentState = NOTIFIEDSTATE;
208                            xmtHandler.notify();
209                        }
210                    }
211
212                    // message is complete, dispatch it !!
213                    log.trace("message complete: {}", msg);
214
215                    // check if this message was supposed to be ignored
216                    // sentList will be empty if preference "LoconetUpdateSlotOnMessageCreation" is not activated
217                    if(trafficController.getSentList().contains(msg)) {
218                        trafficController.getSentList().remove(msg);
219                        log.trace("found packet {} in sentList, ignoring. {} packets in sentList remaining.", msg, trafficController.getSentList().size());
220                    }
221                    else {
222                        log.trace("queue message for notification: {}", msg);
223
224                        final LocoNetMessage thisMsg = msg;
225                        final LnPacketizer thisTc = trafficController;
226                        // return a notification via the queue to ensure end
227                        Runnable r = new Runnable() {
228                            LocoNetMessage msgForLater = thisMsg;
229                            LnPacketizer myTc = thisTc;
230
231                            @Override
232                            public void run() {
233                                myTc.notify(msgForLater);
234                            }
235                        };
236                        javax.swing.SwingUtilities.invokeLater(r);
237                    }
238
239                    // done with this one
240                } catch (LocoNetMessageException e) {
241                    // just let it ride for now
242                    log.warn("run: unexpected LocoNetMessageException: ", e);
243                } catch (java.io.EOFException | java.io.InterruptedIOException e) {
244                    // posted from idle port when enableReceiveTimeout used
245                    // Normal condition, go around the loop again
246                    continue;
247                } catch (java.io.IOException e) {
248                    // fired when write-end of HexFile reaches end
249                    log.debug("IOException, should only happen with HexFile", e);
250                    log.debug("End of file");
251                    disconnectPort(controller);
252                    return;
253                } catch (RuntimeException e) {
254                    // normally, we don't catch RuntimeException, but in this
255                    // permanently running loop it seems wise.
256                    log.warn("run: unexpected Exception", e); // NOI18N
257                    continue;
258                }
259            } // end of permanent loop
260        }
261    }
262
263    LocoNetMessage lastMessage;
264
265    /**
266     * Captive class to handle transmission
267     */
268    class XmtHandler implements Runnable {
269
270        @Override
271        public void run() {
272
273            while (true) {   // loop permanently
274                // any input?
275                try {
276                    // get content; blocks until present
277                    log.debug("check for input");
278                    byte msg[] = null;
279                    lastMessage = null;
280                    msg = xmtList.take();
281                    lastMessage = xmtLocoNetList.remove(); // done second to make sure xmlList had an element
282
283                    //log.debug("-------------------Uhlenbrock IB-COM LocoNet message to SEND: {}", msg.toString());
284
285                    // input - now send
286                    try {
287                        if (ostream != null) {
288                            if (!controller.okToSend()) {
289                                log.debug("LocoNet port not ready to receive");
290                            }
291                            log.debug("start write to stream");
292                            while (!controller.okToSend()) {
293                                Thread.yield();
294                            }
295                            synchronized (xmtHandler) {
296                                mCurrentState = WAITMSGREPLYSTATE;
297                            }
298                            ostream.write(msg);
299                            ostream.flush();
300                            log.debug("end write to stream");
301                            messageTransmitted(msg);
302                            transmitWait(defaultWaitTimer, WAITMSGREPLYSTATE);
303                        } else {
304                            // no stream connected
305                            log.warn("sendLocoNetMessage: no connection established");
306                        }
307                    } catch (java.io.IOException e) {
308                        log.warn("sendLocoNetMessage: IOException: {}", e.toString());
309                    }
310                } catch (InterruptedException ie) {
311                    return; // ending the thread
312                }
313            }
314        }
315    }
316
317    protected void transmitWait(int waitTime, int state/*, String InterruptMessage*/) {
318        // wait() can have spurious wakeup!
319        // so we protect by making sure the entire timeout time is used
320        long currentTime = Calendar.getInstance().getTimeInMillis();
321        long endTime = currentTime + waitTime;
322        while (endTime > (currentTime = Calendar.getInstance().getTimeInMillis())) {
323            long wait = endTime - currentTime;
324            try {
325                synchronized (xmtHandler) {
326                    // Do not wait if the current state has changed since we
327                    // last set it.
328                    if (mCurrentState != state) {
329                        return;
330                    }
331                    xmtHandler.wait(wait); // rcvr normally ends this w state change
332                }
333            } catch (InterruptedException e) {
334                Thread.currentThread().interrupt(); // retain if needed later
335                log.info("Transmit loop interrupted");
336                return;  // If we don't return here, xmtHandler.wait(wait) will be called again, which will cause a new InterruptedException, which results in a loop
337            }
338        }
339        log.debug("Timeout in transmitWait, mCurrentState: {}", mCurrentState);
340    }
341
342    volatile protected int mCurrentState;
343
344    /**
345     * Invoked at startup to start the threads needed here.
346     */
347    @Override
348    public void startThreads() {
349        int priority = Thread.currentThread().getPriority();
350        log.debug("startThreads current priority = {} max available = " + Thread.MAX_PRIORITY + " default = " + Thread.NORM_PRIORITY + " min available = " + Thread.MIN_PRIORITY, priority);
351
352        // make sure that the xmt priority is no lower than the current priority
353        int xmtpriority = (Thread.MAX_PRIORITY - 1 > priority ? Thread.MAX_PRIORITY - 1 : Thread.MAX_PRIORITY);
354        // start the XmtHandler in a thread of its own
355        if (xmtHandler == null) {
356            xmtHandler = new XmtHandler();
357        }
358        xmtThread = new Thread(xmtHandler, "LocoNet Uhlenbrock transmit handler");
359        log.debug("Xmt thread starts at priority {}", xmtpriority);
360        xmtThread.setDaemon(true);
361        xmtThread.setPriority(Thread.MAX_PRIORITY - 1);
362        xmtThread.start();
363
364        // start the RcvHandler in a thread of its own
365        if (rcvHandler == null) {
366            rcvHandler = new RcvHandler(this);
367        }
368        rcvThread = new Thread(rcvHandler, "LocoNet Uhlenbrock receive handler");
369        rcvThread.setDaemon(true);
370        rcvThread.setPriority(Thread.MAX_PRIORITY);
371        rcvThread.start();
372
373    }
374
375    private static final Logger log = LoggerFactory.getLogger(UhlenbrockPacketizer.class);
376
377}