001package jmri.jmrix.loconet.loconetovertcp;
002
003import java.util.StringTokenizer;
004import jmri.jmrix.loconet.LnNetworkPortController;
005import jmri.jmrix.loconet.LnPacketizer;
006import jmri.jmrix.loconet.LocoNetMessage;
007import jmri.jmrix.loconet.LocoNetMessageException;
008import jmri.jmrix.loconet.LocoNetSystemConnectionMemo;
009import org.slf4j.Logger;
010import org.slf4j.LoggerFactory;
011
012/**
013 * Converts Stream-based I/O over the LocoNetOverTcp system network
014 * connection to/from LocoNet messages. The "LocoNetInterface"
015 * side sends/receives LocoNetMessage objects. The connection to a
016 * LnPortnetworkController is via a pair of *Streams, which then carry sequences
017 * of 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
036 * @author Alex Shepherd Copyright (C) 2003, 2006
037 */
038public class LnOverTcpPacketizer extends LnPacketizer {
039
040    static final String RECEIVE_PREFIX = "RECEIVE";
041    static final String SEND_PREFIX = "SEND";
042
043    public LnOverTcpPacketizer(LocoNetSystemConnectionMemo m) {
044        super(m);
045        xmtHandler = new XmtHandler();
046        rcvHandler = new RcvHandler(this);
047    }
048
049    public LnNetworkPortController networkController = null;
050
051    @Override
052    public boolean isXmtBusy() {
053        if (networkController == null) {
054            return false;
055        }
056        return true;
057    }
058
059    /**
060     * Make connection to an existing LnPortnetworkController object.
061     *
062     * @param p Port networkController for connected. Save this for a later
063     *          disconnect call
064     */
065    public void connectPort(LnNetworkPortController p) {
066        istream = p.getInputStream();
067        ostream = p.getOutputStream();
068        if (networkController != null) {
069            log.warn("connectPort: connect called while connected");
070        }
071        networkController = p;
072    }
073
074    /** Starts a new receive thread after a reconnect. */
075    public void restartRcvThread() {
076        rcvThread = jmri.util.ThreadingUtil.newThread(rcvHandler, "LocoNet receive handler"); // NOI18N
077        rcvThread.setDaemon(true);
078        rcvThread.setPriority(Thread.MAX_PRIORITY);
079        rcvThread.start();
080    }
081
082    /**
083     * Break connection to existing LnPortnetworkController object. Once broken,
084     * attempts to send via "message" member will fail.
085     *
086     * @param p previously connected port
087     */
088    public void disconnectPort(LnNetworkPortController p) {
089        istream = null;
090        ostream = null;
091        if (networkController != p) {
092            log.warn("disconnectPort: disconnect called from non-connected LnPortnetworkController");
093        }
094        networkController = null;
095    }
096
097    /**
098     * Captive class to handle incoming characters. This is a permanent loop,
099     * looking for input messages in character form on the stream connected to
100     * the LnPortnetworkController via <code>connectPort</code>.
101     */
102    class RcvHandler implements Runnable {
103
104        /**
105         * Remember the LnPacketizer object.
106         */
107        LnOverTcpPacketizer trafficController;
108
109        public RcvHandler(LnOverTcpPacketizer lt) {
110            trafficController = lt;
111        }
112
113        // readline is deprecated, but there are no problems
114        // with multi-byte characters here.
115        @SuppressWarnings("deprecation")  // InputStream#readline
116        @Override
117        public void run() {
118
119            String rxLine;
120            while (! Thread.interrupted()) {  // loop permanently, program close will exit
121                try {
122                    // Start by looking for a complete line.
123                    // This will block until input is returned, even if the thread is interrupted.
124                    rxLine = istream.readLine();
125                    if (Thread.interrupted()) {
126                        // This indicates normal termination of the thread
127                        // followed by some input being provided by readLine above.
128                        // We return immediately to end the thread, rather than
129                        // processing the no-long-relevant input.
130                        return;
131                    }
132                    if (rxLine == null) {
133                        log.info("run: server closed connection, attempting recovery");
134                        if (trafficController.networkController != null) {
135                            trafficController.networkController.recover();
136                        }
137                        return;
138                    }
139
140                    log.debug("Received: {}", rxLine);
141
142                    StringTokenizer st = new StringTokenizer(rxLine);
143                    if (st.nextToken().equals(RECEIVE_PREFIX)) {
144                        LocoNetMessage msg = null;
145                        int opCode = Integer.parseInt(st.nextToken(), 16);
146                        int byte2 = Integer.parseInt(st.nextToken(), 16);
147
148                        // Decide length
149                        switch ((opCode & 0x60) >> 5) {
150                            default:  // not really possible, but this closes selection for SpotBugs
151                            case 0:
152                                /* 2 byte message */
153
154                                msg = new LocoNetMessage(2);
155                                break;
156
157                            case 1:
158                                /* 4 byte message */
159
160                                msg = new LocoNetMessage(4);
161                                break;
162
163                            case 2:
164                                /* 6 byte message */
165
166                                msg = new LocoNetMessage(6);
167                                break;
168
169                            case 3:
170                                /* N byte message */
171
172                                if (byte2 < 2) {
173                                    log.error("LocoNet message length invalid: {} opcode: {}",
174                                            byte2, Integer.toHexString(opCode));
175                                }
176                                msg = new LocoNetMessage(byte2);
177                                break;
178                        }
179
180                        // message exists, now fill it
181                        msg.setOpCode(opCode);
182                        msg.setElement(1, byte2);
183                        int len = msg.getNumDataElements();
184                        //log.debug("len: {}", len);
185
186                        for (int i = 2; i < len; i++) {
187                            // check for message-blocking error
188                            int b = Integer.parseInt(st.nextToken(), 16);
189                            // log.debug("char {} is: {}", i, Integer.toHexString(b));
190                            if ((b & 0x80) != 0) {
191                                log.warn("LocoNet message with opCode: {} ended early. Expected length: {} seen length: {} unexpected byte: {}", Integer.toHexString(opCode), len, i, Integer.toHexString(b));
192                                throw new LocoNetMessageException();
193                            }
194                            msg.setElement(i, b);
195                        }
196
197                        // message is complete, dispatch it !!
198                        log.trace("message complete: {}", msg);
199                        
200                        // check if this message was supposed to be ignored
201                        // sentList will be empty if preference "LoconetUpdateSlotOnMessageCreation" is not activated
202                        if(trafficController.getSentList().contains(msg)) {
203                            trafficController.getSentList().remove(msg);
204                            log.trace("found packet {} in sentList, ignoring. {} packets in sentList remaining.", msg, trafficController.getSentList().size());
205                        }
206                        else {
207                            log.trace("queue message for notification: {}", msg);
208
209                            final LocoNetMessage thisMsg = msg;
210                            final LnPacketizer thisTc = trafficController;
211                            // return a notification via the queue to ensure end
212                            Runnable r = new Runnable() {
213                                LocoNetMessage msgForLater = thisMsg;
214                                LnPacketizer myTc = thisTc;
215                                
216                                @Override
217                                public void run() {
218                                    myTc.notify(msgForLater);
219                                }
220                            };
221                            javax.swing.SwingUtilities.invokeLater(r);
222                        }
223                    }
224                    // done with this one
225                } catch (LocoNetMessageException e) {
226                    // just let it ride for now
227                    log.warn("run: unexpected LocoNetMessageException: ", e);
228                } catch (java.io.EOFException e) {
229                    // posted from idle port when enableReceiveTimeout used
230                    log.debug("EOFException, is LocoNet serial I/O using timeouts?");
231                } catch (java.io.IOException e) {
232                    // fired when write-end of HexFile reaches end
233                    log.debug("IOException, should only happen with HexFile: ", e);
234                    log.info("End of file");
235//                    disconnectPort(networkController);
236                    return;
237                } // normally, we don't catch RuntimeException, but in this
238                // permanently running loop it seems wise.
239                catch (RuntimeException e) {
240                    log.warn("run: unexpected Exception: ", e);
241                }
242            } // end of permanent loop
243        }
244    }
245
246    /**
247     * Captive class to handle transmission.
248     */
249    class XmtHandler implements Runnable {
250
251        @Override
252        public void run() {
253
254            while (true) {   // loop permanently
255                // any input?
256                try {
257                    // get content; blocks write until present
258                    log.debug("check for input");
259
260                    byte msg[] = xmtList.take();
261
262                    // input - now send
263                    try {
264                        if (ostream != null) {
265                            // Commented out as the original LnPortnetworkController always returned true.
266                            // if (!networkController.okToSend()) log.warn("LocoNet port not ready to receive"); // TCP, not RS232, so message is a real warning
267                            log.debug("start write to stream");
268                            StringBuffer packet = new StringBuffer(msg.length * 3 + SEND_PREFIX.length() + 2);
269                            packet.append(SEND_PREFIX);
270                            String hexString;
271                            for (int Index = 0; Index < msg.length; Index++) {
272                                packet.append(' ');
273                                hexString = Integer.toHexString(msg[Index] & 0xFF).toUpperCase();
274                                if (hexString.length() == 1) {
275                                    packet.append('0');
276                                }
277                                packet.append(hexString);
278                            }
279                            log.debug("Write to LbServer: {}", packet.toString());
280                            packet.append("\r\n");
281                            ostream.write(packet.toString().getBytes());
282                            ostream.flush();
283                            log.debug("end write to stream");
284                        } else {
285                            // no stream connected
286                            log.warn("sendLocoNetMessage: no connection established");
287                        }
288                    } catch (java.io.IOException e) {
289                        log.warn("sendLocoNetMessage: IOException: {}", e.toString());
290                        // write failed; connection likely dropped
291                        if (networkController != null) {
292                            networkController.recover();
293                        }
294                    }
295                } catch (InterruptedException ie) {
296                    return; // ending the thread
297                }
298            }
299        }
300    }
301
302    private static final Logger log = LoggerFactory.getLogger(LnOverTcpPacketizer.class);
303
304}