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