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    /**
075     * Break connection to existing LnPortnetworkController object. Once broken,
076     * attempts to send via "message" member will fail.
077     *
078     * @param p previously connected port
079     */
080    public void disconnectPort(LnNetworkPortController p) {
081        istream = null;
082        ostream = null;
083        if (networkController != p) {
084            log.warn("disconnectPort: disconnect called from non-connected LnPortnetworkController");
085        }
086        networkController = null;
087    }
088
089    /**
090     * Captive class to handle incoming characters. This is a permanent loop,
091     * looking for input messages in character form on the stream connected to
092     * the LnPortnetworkController via <code>connectPort</code>.
093     */
094    class RcvHandler implements Runnable {
095
096        /**
097         * Remember the LnPacketizer object.
098         */
099        LnOverTcpPacketizer trafficController;
100
101        public RcvHandler(LnOverTcpPacketizer lt) {
102            trafficController = lt;
103        }
104
105        // readline is deprecated, but there are no problems
106        // with multi-byte characters here.
107        @SuppressWarnings("deprecation")  // InputStream#readline
108        @Override
109        public void run() {
110
111            String rxLine;
112            while (! Thread.interrupted()) {  // loop permanently, program close will exit
113                try {
114                    // Start by looking for a complete line.
115                    // This will block until input is returned, even if the thread is interrupted.
116                    rxLine = istream.readLine();
117                    if (Thread.interrupted()) {
118                        // This indicates normal termination of the thread
119                        // followed by some input being provided by readLine above.
120                        // We return immediately to end the thread, rather than
121                        // processing the no-long-relevant input.
122                        return;
123                    }
124                    if (rxLine == null) {
125                        log.warn("run: input stream returned null, exiting loop");
126                        return;
127                    }
128
129                    log.debug("Received: {}", rxLine);
130
131                    StringTokenizer st = new StringTokenizer(rxLine);
132                    if (st.nextToken().equals(RECEIVE_PREFIX)) {
133                        LocoNetMessage msg = null;
134                        int opCode = Integer.parseInt(st.nextToken(), 16);
135                        int byte2 = Integer.parseInt(st.nextToken(), 16);
136
137                        // Decide length
138                        switch ((opCode & 0x60) >> 5) {
139                            default:  // not really possible, but this closes selection for SpotBugs
140                            case 0:
141                                /* 2 byte message */
142
143                                msg = new LocoNetMessage(2);
144                                break;
145
146                            case 1:
147                                /* 4 byte message */
148
149                                msg = new LocoNetMessage(4);
150                                break;
151
152                            case 2:
153                                /* 6 byte message */
154
155                                msg = new LocoNetMessage(6);
156                                break;
157
158                            case 3:
159                                /* N byte message */
160
161                                if (byte2 < 2) {
162                                    log.error("LocoNet message length invalid: {} opcode: {}",
163                                            byte2, Integer.toHexString(opCode));
164                                }
165                                msg = new LocoNetMessage(byte2);
166                                break;
167                        }
168
169                        // message exists, now fill it
170                        msg.setOpCode(opCode);
171                        msg.setElement(1, byte2);
172                        int len = msg.getNumDataElements();
173                        //log.debug("len: {}", len);
174
175                        for (int i = 2; i < len; i++) {
176                            // check for message-blocking error
177                            int b = Integer.parseInt(st.nextToken(), 16);
178                            // log.debug("char {} is: {}", i, Integer.toHexString(b));
179                            if ((b & 0x80) != 0) {
180                                log.warn("LocoNet message with opCode: {} ended early. Expected length: {} seen length: {} unexpected byte: {}", Integer.toHexString(opCode), len, i, Integer.toHexString(b));
181                                throw new LocoNetMessageException();
182                            }
183                            msg.setElement(i, b);
184                        }
185
186                        // message is complete, dispatch it !!
187                        if (log.isDebugEnabled()) {
188                            log.debug("queue message for notification");
189                        }
190
191                        final LocoNetMessage thisMsg = msg;
192                        final LnPacketizer thisTc = trafficController;
193                        // return a notification via the queue to ensure end
194                        Runnable r = new Runnable() {
195                            LocoNetMessage msgForLater = thisMsg;
196                            LnPacketizer myTc = thisTc;
197
198                            @Override
199                            public void run() {
200                                myTc.notify(msgForLater);
201                            }
202                        };
203                        javax.swing.SwingUtilities.invokeLater(r);
204                    }
205                    // done with this one
206                } catch (LocoNetMessageException e) {
207                    // just let it ride for now
208                    log.warn("run: unexpected LocoNetMessageException: ", e);
209                } catch (java.io.EOFException e) {
210                    // posted from idle port when enableReceiveTimeout used
211                    log.debug("EOFException, is LocoNet serial I/O using timeouts?");
212                } catch (java.io.IOException e) {
213                    // fired when write-end of HexFile reaches end
214                    log.debug("IOException, should only happen with HexFile: ", e);
215                    log.info("End of file");
216//                    disconnectPort(networkController);
217                    return;
218                } // normally, we don't catch RuntimeException, but in this
219                // permanently running loop it seems wise.
220                catch (RuntimeException e) {
221                    log.warn("run: unexpected Exception: ", e);
222                }
223            } // end of permanent loop
224        }
225    }
226
227    /**
228     * Captive class to handle transmission.
229     */
230    class XmtHandler implements Runnable {
231
232        @Override
233        public void run() {
234
235            while (true) {   // loop permanently
236                // any input?
237                try {
238                    // get content; blocks write until present
239                    log.debug("check for input");
240
241                    byte msg[] = xmtList.take();
242
243                    // input - now send
244                    try {
245                        if (ostream != null) {
246                            // Commented out as the original LnPortnetworkController always returned true.
247                            // if (!networkController.okToSend()) log.warn("LocoNet port not ready to receive"); // TCP, not RS232, so message is a real warning
248                            log.debug("start write to stream");
249                            StringBuffer packet = new StringBuffer(msg.length * 3 + SEND_PREFIX.length() + 2);
250                            packet.append(SEND_PREFIX);
251                            String hexString;
252                            for (int Index = 0; Index < msg.length; Index++) {
253                                packet.append(' ');
254                                hexString = Integer.toHexString(msg[Index] & 0xFF).toUpperCase();
255                                if (hexString.length() == 1) {
256                                    packet.append('0');
257                                }
258                                packet.append(hexString);
259                            }
260                            if (log.isDebugEnabled()) { // Avoid building unneeded Strings
261                                log.debug("Write to LbServer: {}", packet.toString());
262                            }
263                            packet.append("\r\n");
264                            ostream.write(packet.toString().getBytes());
265                            ostream.flush();
266                            log.debug("end write to stream");
267                        } else {
268                            // no stream connected
269                            log.warn("sendLocoNetMessage: no connection established");
270                        }
271                    } catch (java.io.IOException e) {
272                        log.warn("sendLocoNetMessage: IOException: {}", e.toString());
273                    }
274                } catch (InterruptedException ie) {
275                    return; // ending the thread
276                }
277            }
278        }
279    }
280
281    private final static Logger log = LoggerFactory.getLogger(LnOverTcpPacketizer.class);
282
283}