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}