001package jmri.jmrix.loconet.Intellibox;
002
003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
004import jmri.jmrix.loconet.LnPacketizer;
005import jmri.jmrix.loconet.LocoNetMessage;
006import jmri.jmrix.loconet.LocoNetMessageException;
007import jmri.jmrix.loconet.LocoNetSystemConnectionMemo;
008
009/**
010 * Converts Stream-based I/O to/from LocoNet messages. The "LocoNetInterface"
011 * side sends/receives LocoNetMessage objects. The connection to a
012 * LnPortController is via a pair of *Streams, which then carry sequences of
013 * characters for transmission.
014 * <p>
015 * Messages come to this via the main GUI thread, and are forwarded back to
016 * listeners in that same thread. Reception and transmission are handled in
017 * dedicated threads by RcvHandler and XmtHandler objects. Those are internal
018 * classes defined here. The thread priorities are:
019 * <ul>
020 *   <li> RcvHandler - at highest available priority
021 *   <li> XmtHandler - down one, which is assumed to be above the GUI
022 *   <li> (everything else)
023 * </ul>
024 * Some of the message formats used in this class are Copyright Digitrax, Inc.
025 * and used with permission as part of the JMRI project. That permission does
026 * not extend to uses in other software products. If you wish to use this code,
027 * algorithm or these message formats outside of JMRI, please contact Digitrax
028 * Inc for separate permission.
029 *
030 * @author Bob Jacobsen Copyright (C) 2001, 2010
031 */
032public class IBLnPacketizer extends LnPacketizer {
033
034    @SuppressFBWarnings(value = "ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD",
035            justification = "Only used during system initialization")
036    public IBLnPacketizer() {
037        super(new LocoNetSystemConnectionMemo());
038        echo = true;
039    }
040
041    /**
042     * Captive class to handle incoming characters. This is a permanent loop,
043     * looking for input messages in character form on the stream connected to
044     * the LnPortController via <code>connectPort</code>.
045     */
046    class RcvHandler implements Runnable {
047
048        /**
049         * Remember the LnPacketizer object
050         */
051        LnPacketizer trafficController;
052
053        @SuppressFBWarnings(value = "ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD",
054                justification = "single threaded during init; will eventually be replaced for multi-connection support")
055        public RcvHandler(LnPacketizer lt) {
056            trafficController = lt;
057        }
058
059        private byte readNextByteFromUSB() {
060            byte inbyte;
061            while (true) {
062                try {
063                    inbyte = istream.readByte();
064                    return inbyte;
065                } catch (java.io.IOException e) {
066                    continue;
067                }
068            }
069        }
070
071        @Override
072        public void run() {
073
074            int opCode;
075            while (true) {   // loop permanently, program close will exit
076                try {
077                    // start by looking for command -  skip if bit not set
078                    while (((opCode = (readNextByteFromUSB() & 0xFF)) & 0x80) == 0) {
079                        if (log.isDebugEnabled()) { // Avoid building unneeded Strings
080                            log.debug("Skipping: {}", Integer.toHexString(opCode));
081                        }
082                    }
083                    // here opCode is OK. Create output message
084                    if (log.isDebugEnabled()) { // Avoid building unneeded Strings
085                        log.debug("Start message with opcode: {}", Integer.toHexString(opCode));
086                    }
087                    LocoNetMessage msg = null;
088                    while (msg == null) {
089                        try {
090                            // Capture 2nd byte, always present
091                            int byte2 = readNextByteFromUSB() & 0xFF;
092                            //log.debug("Byte2: "+Integer.toHexString(byte2));
093                            if ((byte2 & 0x80) != 0) {
094                                log.warn("LocoNet message with opCode: {} ended early. Byte2 is also an opcode: {}", Integer.toHexString(opCode), Integer.toHexString(byte2));
095                                opCode = byte2;
096                                throw new LocoNetMessageException();
097                            }
098                            // Decide length
099                            switch ((opCode & 0x60) >> 5) {
100                                case 0:
101                                    /* 2 byte message */
102
103                                    msg = new LocoNetMessage(2);
104                                    break;
105
106                                case 1:
107                                    /* 4 byte message */
108
109                                    msg = new LocoNetMessage(4);
110                                    break;
111
112                                case 2:
113                                    /* 6 byte message */
114
115                                    msg = new LocoNetMessage(6);
116                                    break;
117
118                                case 3:
119                                    /* N byte message */
120
121                                    if (byte2 < 2) {
122                                        log.error("LocoNet message length invalid: {} opcode: {}", byte2, Integer.toHexString(opCode));
123                                    }
124                                    msg = new LocoNetMessage(byte2);
125                                    break;
126                                default: // can't happen with this code, but just in case...
127                                    throw new LocoNetMessageException("decode failure " + byte2);
128                            }
129                            // message exists, now fill it
130                            msg.setOpCode(opCode);
131                            msg.setElement(1, byte2);
132                            int len = msg.getNumDataElements();
133                            //log.debug("len: "+len);
134                            for (int i = 2; i < len; i++) {
135                                // check for message-blocking error
136                                int b = readNextByteFromUSB() & 0xFF;
137                                //log.debug("char "+i+" is: "+Integer.toHexString(b));
138                                if ((b & 0x80) != 0) {
139                                    log.warn("LocoNet message with opCode: {} ended early. Expected length: {} seen length: {} unexpected byte: {}", Integer.toHexString(opCode), len, i, Integer.toHexString(b));
140                                    opCode = b;
141                                    throw new LocoNetMessageException();
142                                }
143                                msg.setElement(i, b);
144                            }
145                        } catch (LocoNetMessageException e) {
146                            // retry by going around again
147                            // opCode is set for the newly-started packet
148                            msg = null;
149                            continue;
150                        }
151                    }
152                    // check parity
153                    if (!msg.checkParity()) {
154                        log.warn("Ignore LocoNet packet with bad checksum: {}", msg.toString());
155                        throw new LocoNetMessageException();
156                    }
157                    // message is complete, dispatch it !!
158                    {
159                        if (log.isDebugEnabled()) {
160                            log.debug("queue message for notification");
161                        }
162                        final LocoNetMessage thisMsg = msg;
163                        final LnPacketizer thisTc = trafficController;
164                        // return a notification via the queue to ensure end
165                        Runnable r = new Runnable() {
166                            LocoNetMessage msgForLater = thisMsg;
167                            LnPacketizer myTc = thisTc;
168
169                            @Override
170                            public void run() {
171                                myTc.notify(msgForLater);
172                            }
173                        };
174                        javax.swing.SwingUtilities.invokeLater(r);
175                    }
176
177                    // done with this one
178                } catch (LocoNetMessageException e) {
179                    // just let it ride for now
180                    log.warn("run: unexpected LocoNetMessageException", e);
181                } // normally, we don't catch the unnamed Exception, but in this
182                // permanently running loop it seems wise.
183                catch (Exception e) {
184                    log.warn("run: unexpected Exception", e);
185                }
186            } // end of permanent loop
187        }
188    }
189
190    /**
191     * Captive class to handle transmission
192     */
193    class XmtHandler implements Runnable {
194
195        @Override
196        public void run() {
197
198            while (true) {   // loop permanently
199                // any input?
200                try {
201                    // get content; blocks until present
202                    log.debug("check for input");
203
204                    byte msg[] = xmtList.take();
205
206                    // input - now send
207                    try {
208                        if (ostream != null) {
209                            if (!controller.okToSend()) {
210                                log.debug("LocoNet port not ready to receive");
211                            }
212                            log.debug("start write to stream");
213
214                            // The Intellibox cannot handle messges over 4 bytes without
215                            // stopping the sender via CTS/RTS hardware handshake
216                            // While this should work already by using the normal hardware
217                            // handshake - it doesn't seem to so we need to check/send/flush
218                            // each byte to make sure we don't overflow the IB input buffer
219                            for (int i = 0; i < msg.length; i++) {
220                                while (!controller.okToSend()) {
221                                    Thread.yield();
222                                }
223
224                                ostream.write(msg[i]);
225                                ostream.flush();
226                            }
227
228                            log.debug("end write to stream");
229                            messageTransmitted(msg);
230                        } else {
231                            // no stream connected
232                            log.warn("sendLocoNetMessage: no connection established");
233                        }
234                    } catch (java.io.IOException e) {
235                        log.warn("sendLocoNetMessage: IOException: {}", e.toString());
236                    }
237                } catch (InterruptedException ie) {
238                    return; // ending the thread
239                }
240            }
241        }
242    }
243
244    /**
245     * Invoked at startup to start the threads needed here.
246     */
247    @Override
248    public void startThreads() {
249        int priority = Thread.currentThread().getPriority();
250        log.debug("startThreads current priority = {} max available = " + Thread.MAX_PRIORITY + " default = " + Thread.NORM_PRIORITY + " min available = " + Thread.MIN_PRIORITY, priority);
251
252        // make sure that the xmt priority is no lower than the current priority
253        int xmtpriority = (Thread.MAX_PRIORITY - 1 > priority ? Thread.MAX_PRIORITY - 1 : Thread.MAX_PRIORITY);
254        // start the XmtHandler in a thread of its own
255        if (xmtHandler == null) {
256            xmtHandler = new XmtHandler();
257        }
258        xmtThread = new Thread(xmtHandler, "LocoNet Intellibox transmit handler");
259        log.debug("Xmt thread starts at priority {}", xmtpriority);
260        xmtThread.setDaemon(true);
261        xmtThread.setPriority(Thread.MAX_PRIORITY - 1);
262        xmtThread.start();
263
264        // start the RcvHandler in a thread of its own
265        if (rcvHandler == null) {
266            rcvHandler = new RcvHandler(this);
267        }
268        rcvThread = new Thread(rcvHandler, "LocoNet Intellibox receive handler");
269        rcvThread.setDaemon(true);
270        rcvThread.setPriority(Thread.MAX_PRIORITY);
271        rcvThread.start();
272
273    }
274
275    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(IBLnPacketizer.class);
276}