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}