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}