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                        if (log.isDebugEnabled()) {
199                            log.debug("queue message for notification");
200                        }
201
202                        final LocoNetMessage thisMsg = msg;
203                        final LnPacketizer thisTc = trafficController;
204                        // return a notification via the queue to ensure end
205                        Runnable r = new Runnable() {
206                            LocoNetMessage msgForLater = thisMsg;
207                            LnPacketizer myTc = thisTc;
208
209                            @Override
210                            public void run() {
211                                myTc.notify(msgForLater);
212                            }
213                        };
214                        javax.swing.SwingUtilities.invokeLater(r);
215                    }
216                    // done with this one
217                } catch (LocoNetMessageException e) {
218                    // just let it ride for now
219                    log.warn("run: unexpected LocoNetMessageException: ", e);
220                } catch (java.io.EOFException e) {
221                    // posted from idle port when enableReceiveTimeout used
222                    log.debug("EOFException, is LocoNet serial I/O using timeouts?");
223                } catch (java.io.IOException e) {
224                    // fired when write-end of HexFile reaches end
225                    log.debug("IOException, should only happen with HexFile: ", e);
226                    log.info("End of file");
227//                    disconnectPort(networkController);
228                    return;
229                } // normally, we don't catch RuntimeException, but in this
230                // permanently running loop it seems wise.
231                catch (RuntimeException e) {
232                    log.warn("run: unexpected Exception: ", e);
233                }
234            } // end of permanent loop
235        }
236    }
237
238    /**
239     * Captive class to handle transmission.
240     */
241    class XmtHandler implements Runnable {
242
243        @Override
244        public void run() {
245
246            while (true) {   // loop permanently
247                // any input?
248                try {
249                    // get content; blocks write until present
250                    log.debug("check for input");
251
252                    byte msg[] = xmtList.take();
253
254                    // input - now send
255                    try {
256                        if (ostream != null) {
257                            // Commented out as the original LnPortnetworkController always returned true.
258                            // if (!networkController.okToSend()) log.warn("LocoNet port not ready to receive"); // TCP, not RS232, so message is a real warning
259                            log.debug("start write to stream");
260                            StringBuffer packet = new StringBuffer(msg.length * 3 + SEND_PREFIX.length() + 2);
261                            packet.append(SEND_PREFIX);
262                            String hexString;
263                            for (int Index = 0; Index < msg.length; Index++) {
264                                packet.append(' ');
265                                hexString = Integer.toHexString(msg[Index] & 0xFF).toUpperCase();
266                                if (hexString.length() == 1) {
267                                    packet.append('0');
268                                }
269                                packet.append(hexString);
270                            }
271                            if (log.isDebugEnabled()) { // Avoid building unneeded Strings
272                                log.debug("Write to LbServer: {}", packet.toString());
273                            }
274                            packet.append("\r\n");
275                            ostream.write(packet.toString().getBytes());
276                            ostream.flush();
277                            log.debug("end write to stream");
278                        } else {
279                            // no stream connected
280                            log.warn("sendLocoNetMessage: no connection established");
281                        }
282                    } catch (java.io.IOException e) {
283                        log.warn("sendLocoNetMessage: IOException: {}", e.toString());
284                        // write failed; connection likely dropped
285                        if (networkController != null) {
286                            networkController.recover();
287                        }
288                    }
289                } catch (InterruptedException ie) {
290                    return; // ending the thread
291                }
292            }
293        }
294    }
295
296    private static final Logger log = LoggerFactory.getLogger(LnOverTcpPacketizer.class);
297
298}