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                        log.debug("Skipping: {}", Integer.toHexString(opCode));
080                    }
081                    // here opCode is OK. Create output message
082                    log.debug("Start message with opcode: {}", Integer.toHexString(opCode));
083                    LocoNetMessage msg = null;
084                    while (msg == null) {
085                        try {
086                            // Capture 2nd byte, always present
087                            int byte2 = readNextByteFromUSB() & 0xFF;
088                            //log.debug("Byte2: "+Integer.toHexString(byte2));
089                            if ((byte2 & 0x80) != 0) {
090                                log.warn("LocoNet message with opCode: {} ended early. Byte2 is also an opcode: {}", Integer.toHexString(opCode), Integer.toHexString(byte2));
091                                opCode = byte2;
092                                throw new LocoNetMessageException();
093                            }
094                            // Decide length
095                            switch ((opCode & 0x60) >> 5) {
096                                case 0:
097                                    /* 2 byte message */
098
099                                    msg = new LocoNetMessage(2);
100                                    break;
101
102                                case 1:
103                                    /* 4 byte message */
104
105                                    msg = new LocoNetMessage(4);
106                                    break;
107
108                                case 2:
109                                    /* 6 byte message */
110
111                                    msg = new LocoNetMessage(6);
112                                    break;
113
114                                case 3:
115                                    /* N byte message */
116
117                                    if (byte2 < 2) {
118                                        log.error("LocoNet message length invalid: {} opcode: {}", byte2, Integer.toHexString(opCode));
119                                    }
120                                    msg = new LocoNetMessage(byte2);
121                                    break;
122                                default: // can't happen with this code, but just in case...
123                                    throw new LocoNetMessageException("decode failure " + byte2);
124                            }
125                            // message exists, now fill it
126                            msg.setOpCode(opCode);
127                            msg.setElement(1, byte2);
128                            int len = msg.getNumDataElements();
129                            //log.debug("len: "+len);
130                            for (int i = 2; i < len; i++) {
131                                // check for message-blocking error
132                                int b = readNextByteFromUSB() & 0xFF;
133                                //log.debug("char "+i+" is: "+Integer.toHexString(b));
134                                if ((b & 0x80) != 0) {
135                                    log.warn("LocoNet message with opCode: {} ended early. Expected length: {} seen length: {} unexpected byte: {}", Integer.toHexString(opCode), len, i, Integer.toHexString(b));
136                                    opCode = b;
137                                    throw new LocoNetMessageException();
138                                }
139                                msg.setElement(i, b);
140                            }
141                        } catch (LocoNetMessageException e) {
142                            // retry by going around again
143                            // opCode is set for the newly-started packet
144                            msg = null;
145                            continue;
146                        }
147                    }
148                    // check parity
149                    if (!msg.checkParity()) {
150                        log.warn("Ignore LocoNet packet with bad checksum: {}", msg.toString());
151                        throw new LocoNetMessageException();
152                    }
153                    // message is complete, dispatch it !!
154                    log.trace("message complete: {}", msg);
155                        
156                    // check if this message was supposed to be ignored
157                    // sentList will be empty if preference "LoconetUpdateSlotOnMessageCreation" is not activated
158                    if(trafficController.getSentList().contains(msg)) {
159                        trafficController.getSentList().remove(msg);
160                        log.trace("found packet {} in sentList, ignoring. {} packets in sentList remaining.", msg, trafficController.getSentList().size());
161                    }
162                    else {
163                        log.trace("queue message for notification: {}", msg);
164
165                        final LocoNetMessage thisMsg = msg;
166                        final LnPacketizer thisTc = trafficController;
167                        // return a notification via the queue to ensure end
168                        Runnable r = new Runnable() {
169                            LocoNetMessage msgForLater = thisMsg;
170                            LnPacketizer myTc = thisTc;
171                            
172                            @Override
173                            public void run() {
174                                myTc.notify(msgForLater);
175                            }
176                        };
177                        javax.swing.SwingUtilities.invokeLater(r);
178                    }
179
180                    // done with this one
181                } catch (LocoNetMessageException e) {
182                    // just let it ride for now
183                    log.warn("run: unexpected LocoNetMessageException", e);
184                } // normally, we don't catch the unnamed Exception, but in this
185                // permanently running loop it seems wise.
186                catch (Exception e) {
187                    log.warn("run: unexpected Exception", e);
188                }
189            } // end of permanent loop
190        }
191    }
192
193    /**
194     * Captive class to handle transmission
195     */
196    class XmtHandler implements Runnable {
197
198        @Override
199        public void run() {
200
201            while (true) {   // loop permanently
202                // any input?
203                try {
204                    // get content; blocks until present
205                    log.debug("check for input");
206
207                    byte msg[] = xmtList.take();
208
209                    // input - now send
210                    try {
211                        if (ostream != null) {
212                            if (!controller.okToSend()) {
213                                log.debug("LocoNet port not ready to receive");
214                            }
215                            log.debug("start write to stream");
216
217                            // The Intellibox cannot handle messges over 4 bytes without
218                            // stopping the sender via CTS/RTS hardware handshake
219                            // While this should work already by using the normal hardware
220                            // handshake - it doesn't seem to so we need to check/send/flush
221                            // each byte to make sure we don't overflow the IB input buffer
222                            for (int i = 0; i < msg.length; i++) {
223                                while (!controller.okToSend()) {
224                                    Thread.yield();
225                                }
226
227                                ostream.write(msg[i]);
228                                ostream.flush();
229                            }
230
231                            log.debug("end write to stream");
232                            messageTransmitted(msg);
233                        } else {
234                            // no stream connected
235                            log.warn("sendLocoNetMessage: no connection established");
236                        }
237                    } catch (java.io.IOException e) {
238                        log.warn("sendLocoNetMessage: IOException: {}", e.toString());
239                    }
240                } catch (InterruptedException ie) {
241                    return; // ending the thread
242                }
243            }
244        }
245    }
246
247    /**
248     * Invoked at startup to start the threads needed here.
249     */
250    @Override
251    public void startThreads() {
252        int priority = Thread.currentThread().getPriority();
253        log.debug("startThreads current priority = {} max available = " + Thread.MAX_PRIORITY + " default = " + Thread.NORM_PRIORITY + " min available = " + Thread.MIN_PRIORITY, priority);
254
255        // make sure that the xmt priority is no lower than the current priority
256        int xmtpriority = (Thread.MAX_PRIORITY - 1 > priority ? Thread.MAX_PRIORITY - 1 : Thread.MAX_PRIORITY);
257        // start the XmtHandler in a thread of its own
258        if (xmtHandler == null) {
259            xmtHandler = new XmtHandler();
260        }
261        xmtThread = new Thread(xmtHandler, "LocoNet Intellibox transmit handler");
262        log.debug("Xmt thread starts at priority {}", xmtpriority);
263        xmtThread.setDaemon(true);
264        xmtThread.setPriority(Thread.MAX_PRIORITY - 1);
265        xmtThread.start();
266
267        // start the RcvHandler in a thread of its own
268        if (rcvHandler == null) {
269            rcvHandler = new RcvHandler(this);
270        }
271        rcvThread = new Thread(rcvHandler, "LocoNet Intellibox receive handler");
272        rcvThread.setDaemon(true);
273        rcvThread.setPriority(Thread.MAX_PRIORITY);
274        rcvThread.start();
275
276    }
277
278    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(IBLnPacketizer.class);
279}