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.info("run: server closed connection, attempting recovery");
126                        if (trafficController.networkController != null) {
127                            trafficController.networkController.recover();
128                        }
129                        return;
130                    }
131
132                    log.debug("Received: {}", rxLine);
133
134                    StringTokenizer st = new StringTokenizer(rxLine);
135                    if (st.nextToken().equals(RECEIVE_PREFIX)) {
136                        LocoNetMessage msg = null;
137                        int opCode = Integer.parseInt(st.nextToken(), 16);
138                        int byte2 = Integer.parseInt(st.nextToken(), 16);
139
140                        // Decide length
141                        switch ((opCode & 0x60) >> 5) {
142                            default:  // not really possible, but this closes selection for SpotBugs
143                            case 0:
144                                /* 2 byte message */
145
146                                msg = new LocoNetMessage(2);
147                                break;
148
149                            case 1:
150                                /* 4 byte message */
151
152                                msg = new LocoNetMessage(4);
153                                break;
154
155                            case 2:
156                                /* 6 byte message */
157
158                                msg = new LocoNetMessage(6);
159                                break;
160
161                            case 3:
162                                /* N byte message */
163
164                                if (byte2 < 2) {
165                                    log.error("LocoNet message length invalid: {} opcode: {}",
166                                            byte2, Integer.toHexString(opCode));
167                                }
168                                msg = new LocoNetMessage(byte2);
169                                break;
170                        }
171
172                        // message exists, now fill it
173                        msg.setOpCode(opCode);
174                        msg.setElement(1, byte2);
175                        int len = msg.getNumDataElements();
176                        //log.debug("len: {}", len);
177
178                        for (int i = 2; i < len; i++) {
179                            // check for message-blocking error
180                            int b = Integer.parseInt(st.nextToken(), 16);
181                            // log.debug("char {} is: {}", i, Integer.toHexString(b));
182                            if ((b & 0x80) != 0) {
183                                log.warn("LocoNet message with opCode: {} ended early. Expected length: {} seen length: {} unexpected byte: {}", Integer.toHexString(opCode), len, i, Integer.toHexString(b));
184                                throw new LocoNetMessageException();
185                            }
186                            msg.setElement(i, b);
187                        }
188
189                        // message is complete, dispatch it !!
190                        log.trace("message complete: {}", msg);
191                        
192                        // check if this message was supposed to be ignored
193                        // sentList will be empty if preference "LoconetUpdateSlotOnMessageCreation" is not activated
194                        if(trafficController.getSentList().contains(msg)) {
195                            trafficController.getSentList().remove(msg);
196                            log.trace("found packet {} in sentList, ignoring. {} packets in sentList remaining.", msg, trafficController.getSentList().size());
197                        }
198                        else {
199                            log.trace("queue message for notification: {}", msg);
200
201                            final LocoNetMessage thisMsg = msg;
202                            final LnPacketizer thisTc = trafficController;
203                            // return a notification via the queue to ensure end
204                            Runnable r = new Runnable() {
205                                LocoNetMessage msgForLater = thisMsg;
206                                LnPacketizer myTc = thisTc;
207                                
208                                @Override
209                                public void run() {
210                                    myTc.notify(msgForLater);
211                                }
212                            };
213                            javax.swing.SwingUtilities.invokeLater(r);
214                        }
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                            log.debug("Write to LbServer: {}", packet.toString());
272                            packet.append("\r\n");
273                            ostream.write(packet.toString().getBytes());
274                            ostream.flush();
275                            log.debug("end write to stream");
276                        } else {
277                            // no stream connected
278                            log.warn("sendLocoNetMessage: no connection established");
279                        }
280                    } catch (java.io.IOException e) {
281                        log.warn("sendLocoNetMessage: IOException: {}", e.toString());
282                        // write failed; connection likely dropped
283                        if (networkController != null) {
284                            networkController.recover();
285                        }
286                    }
287                } catch (InterruptedException ie) {
288                    return; // ending the thread
289                }
290            }
291        }
292    }
293
294    private static final Logger log = LoggerFactory.getLogger(LnOverTcpPacketizer.class);
295
296}