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     */
059    @Override
060    public void sendLocoNetMessage(LocoNetMessage m) {
061        log.debug("add to queue message {}", m.toString());
062        // update statistics
063        transmittedMsgCount++;
064
065        // set the error correcting code byte(s) before transmittal
066        m.setParity();
067
068        // stream to port in single write, as that's needed by serial
069        int len = m.getNumDataElements();
070        byte msg[] = new byte[len];
071        for (int i = 0; i < len; i++) {
072            msg[i] = (byte) m.getElement(i);
073        }
074
075        if (log.isDebugEnabled()) {
076            log.debug("queue LocoNet packet: {}", m.toString());
077        }
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        } catch (RuntimeException e) {
083            log.warn("passing to xmit: unexpected exception: ", e);
084        }
085    }
086
087    /**
088     * Synchronized list used as a transmit queue.
089     * <p>
090     * This is public to allow access from the internal class(es) when compiling
091     * with Java 1.1
092     */
093    public ConcurrentLinkedQueue<LocoNetMessage> xmtLocoNetList = new ConcurrentLinkedQueue<>();
094
095    /**
096     * Captive class to handle incoming characters. This is a permanent loop,
097     * looking for input messages in character form on the stream connected to
098     * the LnPortController via <code>connectPort</code>.
099     */
100    class RcvHandler implements Runnable {
101
102        /**
103         * Remember the LnPacketizer object.
104         */
105        LnPacketizer trafficController;
106
107        public RcvHandler(LnPacketizer lt) {
108            trafficController = lt;
109        }
110
111        @Override
112        public void run() {
113
114            int opCode;
115            while (true) {   // loop permanently, program close will exit
116                try {
117                    // start by looking for command -  skip if bit not set
118                    int inbyte = readByteProtected(istream) & 0xFF;
119                    while (((opCode = (inbyte)) & 0x80) == 0) {
120                        log.debug("Skipping: {}", Integer.toHexString(opCode));
121                        inbyte = readByteProtected(istream) & 0xFF;
122                    }
123                    // here opCode is OK. Create output message
124                    log.debug("Start message with opcode: {}", Integer.toHexString(opCode));
125                    LocoNetMessage msg = null;
126                    while (msg == null) {
127                        try {
128                            // Capture 2nd byte, always present
129                            int byte2 = readByteProtected(istream) & 0xFF;
130                            //log.debug("Byte2: "+Integer.toHexString(byte2));
131                            if ((byte2 & 0x80) != 0) {
132                                log.warn("LocoNet message with opCode: {} ended early. Byte2 is also an opcode: {}", Integer.toHexString(opCode), Integer.toHexString(byte2));
133                                opCode = byte2;
134                                throw new LocoNetMessageException();
135                            }
136
137                            // Decide length
138                            switch ((opCode & 0x60) >> 5) {
139                                case 0:
140                                    /* 2 byte message */
141
142                                    msg = new LocoNetMessage(2);
143                                    break;
144
145                                case 1:
146                                    /* 4 byte message */
147
148                                    msg = new LocoNetMessage(4);
149                                    break;
150
151                                case 2:
152                                    /* 6 byte message */
153
154                                    msg = new LocoNetMessage(6);
155                                    break;
156
157                                case 3:
158                                    /* N byte message */
159
160                                    if (byte2 < 2) {
161                                        log.error("LocoNet message length invalid: {} opcode: {}", byte2, Integer.toHexString(opCode));
162                                    }
163                                    msg = new LocoNetMessage(byte2);
164                                    break;
165                                default: // can't happen with this code, but just in case...
166                                    throw new LocoNetMessageException("decode failure " + byte2);
167                            }
168                            // message exists, now fill it
169                            msg.setOpCode(opCode);
170                            msg.setElement(1, byte2);
171                            int len = msg.getNumDataElements();
172                            //log.debug("len: "+len);
173                            for (int i = 2; i < len; i++) {
174                                // check for message-blocking error
175                                int b = readByteProtected(istream) & 0xFF;
176                                //log.debug("char "+i+" is: "+Integer.toHexString(b));
177                                if ((b & 0x80) != 0) {
178                                    log.warn("LocoNet message with opCode: {} ended early. Expected length: {} seen length: {} unexpected byte: {}", Integer.toHexString(opCode), len, i, Integer.toHexString(b));
179                                    opCode = b;
180                                    throw new LocoNetMessageException();
181                                }
182                                msg.setElement(i, b);
183                            }
184                        } catch (LocoNetMessageException e) {
185                            // retry by going around again
186                            // opCode is set for the newly-started packet
187                            msg = null;
188                            continue;
189                        }
190                    }
191                    // check parity
192                    if (!msg.checkParity()) {
193                        log.warn("Ignore LocoNet packet with bad checksum: {}", msg.toString());
194                        throw new LocoNetMessageException();
195                    }
196
197                    if (msg.equals(lastMessage)) {
198                        log.debug("We have our returned message and can send back out our next instruction");
199                        mCurrentState = NOTIFIEDSTATE;
200                    }
201
202                    // message is complete, dispatch it !!
203                    {
204                        log.debug("queue message for notification");
205                        //log.debug("-------------------Uhlenbrock IB-COM LocoNet message RECEIVED: {}", msg.toString());
206                        final LocoNetMessage thisMsg = msg;
207                        final LnPacketizer thisTc = trafficController;
208                        // return a notification via the queue to ensure end
209                        Runnable r = new Runnable() {
210                            LocoNetMessage msgForLater = thisMsg;
211                            LnPacketizer myTc = thisTc;
212
213                            @Override
214                            public void run() {
215                                myTc.notify(msgForLater);
216                            }
217                        };
218                        javax.swing.SwingUtilities.invokeLater(r);
219                    }
220
221                    // done with this one
222                } catch (LocoNetMessageException e) {
223                    // just let it ride for now
224                    log.warn("run: unexpected LocoNetMessageException: ", e);
225                } catch (java.io.EOFException e) {
226                    // posted from idle port when enableReceiveTimeout used
227                    log.debug("EOFException, is LocoNet serial I/O using timeouts?");
228                } catch (java.io.IOException e) {
229                    // fired when write-end of HexFile reaches end
230                    log.debug("IOException, should only happen with HexFile", e);
231                    log.debug("End of file");
232                    disconnectPort(controller);
233                    return;
234                } // normally, we don't catch the unnamed Exception, but in this
235                // permanently running loop it seems wise.
236                catch (RuntimeException e) {
237                    log.warn("run: unexpected Exception", e);
238                }
239            } // end of permanent loop
240        }
241    }
242
243    LocoNetMessage lastMessage;
244
245    /**
246     * Captive class to handle transmission
247     */
248    class XmtHandler implements Runnable {
249
250        @Override
251        public void run() {
252
253            while (true) {   // loop permanently
254                // any input?
255                try {
256                    // get content; blocks until present
257                    log.debug("check for input");
258                    byte msg[] = null;
259                    lastMessage = null;
260                    msg = xmtList.take();
261                    lastMessage = xmtLocoNetList.remove(); // done second to make sure xmlList had an element
262
263                    //log.debug("-------------------Uhlenbrock IB-COM LocoNet message to SEND: {}", msg.toString());
264
265                    // input - now send
266                    try {
267                        if (ostream != null) {
268                            if (!controller.okToSend()) {
269                                log.debug("LocoNet port not ready to receive");
270                            }
271                            log.debug("start write to stream");
272                            while (!controller.okToSend()) {
273                                Thread.yield();
274                            }
275                            ostream.write(msg);
276                            ostream.flush();
277                            log.debug("end write to stream");
278                            messageTransmitted(msg);
279                            mCurrentState = WAITMSGREPLYSTATE;
280                            transmitWait(defaultWaitTimer, WAITMSGREPLYSTATE);
281                        } else {
282                            // no stream connected
283                            log.warn("sendLocoNetMessage: no connection established");
284                        }
285                    } catch (java.io.IOException e) {
286                        log.warn("sendLocoNetMessage: IOException: {}", e.toString());
287                    }
288                } catch (InterruptedException ie) {
289                    return; // ending the thread
290                }
291            }
292        }
293    }
294
295    protected void transmitWait(int waitTime, int state/*, String InterruptMessage*/) {
296        // wait() can have spurious wakeup!
297        // so we protect by making sure the entire timeout time is used
298        long currentTime = Calendar.getInstance().getTimeInMillis();
299        long endTime = currentTime + waitTime;
300        while (endTime > (currentTime = Calendar.getInstance().getTimeInMillis())) {
301            long wait = endTime - currentTime;
302            try {
303                synchronized (xmtHandler) {
304                    // Do not wait if the current state has changed since we
305                    // last set it.
306                    if (mCurrentState != state) {
307                        return;
308                    }
309                    xmtHandler.wait(wait); // rcvr normally ends this w state change
310                }
311            } catch (InterruptedException e) {
312                Thread.currentThread().interrupt(); // retain if needed later
313                log.error("transmitLoop interrupted");
314            }
315        }
316        log.debug("Timeout in transmitWait, mCurrentState: {}", mCurrentState);
317    }
318
319    volatile protected int mCurrentState;
320
321    /**
322     * Invoked at startup to start the threads needed here.
323     */
324    @Override
325    public void startThreads() {
326        int priority = Thread.currentThread().getPriority();
327        log.debug("startThreads current priority = {} max available = " + Thread.MAX_PRIORITY + " default = " + Thread.NORM_PRIORITY + " min available = " + Thread.MIN_PRIORITY, priority);
328
329        // make sure that the xmt priority is no lower than the current priority
330        int xmtpriority = (Thread.MAX_PRIORITY - 1 > priority ? Thread.MAX_PRIORITY - 1 : Thread.MAX_PRIORITY);
331        // start the XmtHandler in a thread of its own
332        if (xmtHandler == null) {
333            xmtHandler = new XmtHandler();
334        }
335        xmtThread = new Thread(xmtHandler, "LocoNet Uhlenbrock transmit handler");
336        log.debug("Xmt thread starts at priority {}", xmtpriority);
337        xmtThread.setDaemon(true);
338        xmtThread.setPriority(Thread.MAX_PRIORITY - 1);
339        xmtThread.start();
340
341        // start the RcvHandler in a thread of its own
342        if (rcvHandler == null) {
343            rcvHandler = new RcvHandler(this);
344        }
345        rcvThread = new Thread(rcvHandler, "LocoNet Uhlenbrock receive handler");
346        rcvThread.setDaemon(true);
347        rcvThread.setPriority(Thread.MAX_PRIORITY);
348        rcvThread.start();
349
350    }
351
352    private final static Logger log = LoggerFactory.getLogger(UhlenbrockPacketizer.class);
353
354}