001package jmri.jmrix.loconet; 002 003import java.io.DataInputStream; 004import java.io.EOFException; 005import java.io.OutputStream; 006import java.util.concurrent.LinkedTransferQueue; 007import org.slf4j.Logger; 008import org.slf4j.LoggerFactory; 009 010/** 011 * Converts Stream-based I/O to/from LocoNet messages. The "LocoNetInterface" 012 * side sends/receives LocoNetMessage objects. The connection to a 013 * LnPortController is via a pair of *Streams, which then carry sequences of 014 * characters for transmission. 015 * <p> 016 * Messages come to this via the main GUI thread, and are forwarded back to 017 * listeners in that same thread. Reception and transmission are handled in 018 * dedicated threads by RcvHandler and XmtHandler objects. Those are internal 019 * classes defined here. The thread priorities are: 020 * <ul> 021 * <li> RcvHandler - at highest available priority 022 * <li> XmtHandler - down one, which is assumed to be above the GUI 023 * <li> (everything else) 024 * </ul> 025 * Some of the message formats used in this class are Copyright Digitrax, Inc. 026 * and used with permission as part of the JMRI project. That permission does 027 * not extend to uses in other software products. If you wish to use this code, 028 * algorithm or these message formats outside of JMRI, please contact Digitrax 029 * Inc for separate permission. 030 * 031 * @author Bob Jacobsen Copyright (C) 2001, 2018 032 * @author B. Milhaupt Copyright (C) 2020 033 */ 034public class LnPacketizer extends LnTrafficController { 035 036 /** 037 * True if the external hardware is not echoing messages, so we must. 038 */ 039 protected boolean echo = false; // true = echo messages here, instead of in hardware 040 041 public LnPacketizer(LocoNetSystemConnectionMemo m) { 042 // set the memo to point here 043 memo = m; 044 m.setLnTrafficController(this); 045 } 046 047 // The methods to implement the LocoNetInterface 048 049 /** 050 * {@inheritDoc} 051 */ 052 @Override 053 public boolean status() { 054 boolean returnVal = ( ostream != null && istream != null 055 && xmtThread != null && xmtThread.isAlive() && xmtHandler != null 056 && rcvThread != null && rcvThread.isAlive() && rcvHandler != null 057 ); 058 return returnVal; 059 } 060 061 /** 062 * Synchronized list used as a transmit queue. 063 */ 064 protected LinkedTransferQueue<byte[]> xmtList = new LinkedTransferQueue<>(); 065 066 /** 067 * XmtHandler (a local class) object to implement the transmit thread. 068 * <p> 069 * We create this object in startThreads() as each packetizer uses different handlers. 070 * So long as the object is created before using it to sync it works. 071 * 072 */ 073 protected Runnable xmtHandler = null; 074 075 /** 076 * RcvHandler (a local class) object to implement the receive thread 077 */ 078 protected Runnable rcvHandler; 079 080 /** 081 * Forward a preformatted LocoNetMessage to the actual interface. 082 * <p> 083 * Checksum is computed and overwritten here, then the message is converted 084 * to a byte array and queued for transmission. 085 * 086 * @param m Message to send; will be updated with CRC 087 * @param requestIgnoreEcho If true: Notify listeners on enqueing message, ignore echo from line. 088 * Only in effect if preference "LoconetUpdateSlotOnMessageCreation" is set. 089 */ 090 @Override 091 public void sendLocoNetMessage(LocoNetMessage m, boolean requestIgnoreEcho) { 092 093 // update statistics 094 transmittedMsgCount++; 095 096 // set the error correcting code byte(s) before transmittal 097 m.setParity(); 098 099 // stream to port in single write, as that's needed by serial 100 int len = m.getNumDataElements(); 101 byte[] msg = new byte[len]; 102 for (int i = 0; i < len; i++) { 103 msg[i] = (byte) m.getElement(i); 104 } 105 106 log.debug("queue LocoNet packet: {}", m); 107 // We need to queue the request and wake the xmit thread in an atomic operation 108 // But the thread might not be running, in which case the request is just 109 // queued up. 110 try { 111 xmtList.add(msg); 112 // save to queue if we want to remember it to check in receive handler 113 if (mLoconetUpdateSlotOnMessageCreation && requestIgnoreEcho) { 114 log.trace("add LocoNet packet {} to sentList. Now {} packets in sentList.", m, sentList.size()); 115 sentList.add(m); 116 log.trace("queue message for notification: {}", m); 117 jmri.util.ThreadingUtil.runOnLayoutEventually(new RcvMemo(m, this)); 118 } 119 } catch (RuntimeException e) { 120 log.warn("passing to xmit: unexpected exception: ", e); 121 } 122 123 } 124 125 /** 126 * Implement abstract method to signal if there's a backlog of information 127 * waiting to be sent. 128 * 129 * @return true if busy, false if nothing waiting to send 130 */ 131 @Override 132 public boolean isXmtBusy() { 133 if (controller == null) { 134 return false; 135 } 136 137 return (!controller.okToSend()); 138 } 139 140 // methods to connect/disconnect to a source of data in a LnPortController 141 142 protected LnPortController controller = null; 143 144 /** 145 * Make connection to an existing LnPortController object. 146 * 147 * @param p Port controller for connected. Save this for a later disconnect 148 * call 149 */ 150 public void connectPort(LnPortController p) { 151 istream = p.getInputStream(); 152 ostream = p.getOutputStream(); 153 if (controller != null) { 154 log.warn("connectPort: connect called while connected"); 155 } 156 controller = p; 157 } 158 159 /** Starts a new receive thread after a reconnect. */ 160 public void restartRcvThread() { 161 rcvThread = jmri.util.ThreadingUtil.newThread(rcvHandler, "LocoNet receive handler"); // NOI18N 162 rcvThread.setDaemon(true); 163 rcvThread.setPriority(Thread.MAX_PRIORITY); 164 rcvThread.start(); 165 } 166 167 /** 168 * Break connection to an existing LnPortController object. Once broken, 169 * attempts to send via "message" member will fail. 170 * 171 * @param p previously connected port 172 */ 173 public void disconnectPort(LnPortController p) { 174 istream = null; 175 ostream = null; 176 if (controller != p) { 177 log.warn("disconnectPort: disconnect called from non-connected LnPortController"); 178 } 179 controller = null; 180 } 181 182 // data members to hold the streams. These are public so the inner classes defined here 183 // can access them with a Java 1.1 compiler 184 public DataInputStream istream = null; 185 public OutputStream ostream = null; 186 187 /** 188 * Read a single byte, protecting against various timeouts, etc. 189 * <p> 190 * When a port is set to have a receive timeout (via the 191 * enableReceiveTimeout() method), some will return zero bytes or an 192 * EOFException at the end of the timeout. In that case, the read should be 193 * repeated to get the next real character. 194 * 195 * @param istream stream to read from 196 * @return buffer of received data 197 * @throws java.io.IOException failure during stream read 198 * 199 */ 200 protected byte readByteProtected(DataInputStream istream) throws java.io.IOException { 201 while (true) { // loop will repeat until character found 202 int nchars; 203 // The istream should be configured so that the following 204 // read(..) call only blocks for a short time, e.g. 100msec, if no 205 // data is available. It's OK if it 206 // throws e.g. java.io.InterruptedIOException 207 // in that case, as the calling loop should just go around 208 // and request input again. This semi-blocking behavior will 209 // let the terminateThreads() method end this thread cleanly. 210 nchars = istream.read(rcvBuffer, 0, 1); 211 if (nchars < 0) { 212 throw new EOFException(String.format("Stream read returned %d, indicating end-of-file", nchars)); 213 } 214 if (nchars > 0) { 215 return rcvBuffer[0]; 216 } 217 } 218 } 219 // Defined this way to reduce new object creation 220 private final byte[] rcvBuffer = new byte[1]; 221 222 /** 223 * Captive class to handle incoming characters. This is a permanent loop, 224 * looking for input messages in character form on the stream connected to 225 * the LnPortController via <code>connectPort</code>. 226 */ 227 protected class RcvHandler implements Runnable { 228 229 /** 230 * Remember the LnPacketizer object 231 */ 232 LnTrafficController trafficController; 233 234 public RcvHandler(LnTrafficController lt) { 235 trafficController = lt; 236 } 237 238 /** 239 * Handle incoming characters. This is a permanent loop, looking for 240 * input messages in character form on the stream connected to the 241 * LnPortController via <code>connectPort</code>. Terminates with the 242 * input stream breaking out of the try block. 243 */ 244 @Override 245 public void run() { 246 247 int opCode; 248 while (!threadStopRequest && ! Thread.interrupted() ) { // loop until asked to stop 249 try { 250 // start by looking for command - skip if bit not set 251 while (((opCode = (readByteProtected(istream) & 0xFF)) & 0x80) == 0) { // the real work is in the loop check 252 log.trace("Skipping: {}", Integer.toHexString(opCode)); // NOI18N 253 } 254 // here opCode is OK. Create output message 255 log.trace(" (RcvHandler) Start message with opcode: {}", Integer.toHexString(opCode)); // NOI18N 256 LocoNetMessage msg = null; 257 while (msg == null) { 258 try { 259 // Capture 2nd byte, always present 260 int byte2 = readByteProtected(istream) & 0xFF; 261 log.trace("Byte2: {}", Integer.toHexString(byte2)); // NOI18N 262 int len = 2; 263 switch ((opCode & 0x60) >> 5) { 264 case 0: 265 /* 2 byte message */ 266 267 len = 2; 268 break; 269 270 case 1: 271 /* 4 byte message */ 272 273 len = 4; 274 break; 275 276 case 2: 277 /* 6 byte message */ 278 279 len = 6; 280 break; 281 282 case 3: 283 /* N byte message */ 284 285 if (byte2 < 2) { 286 log.error("LocoNet message length invalid: {} opcode: {}", byte2, Integer.toHexString(opCode)); // NOI18N 287 } 288 len = byte2; 289 break; 290 default: 291 log.warn("Unhandled code: {}", (opCode & 0x60) >> 5); 292 break; 293 } 294 msg = new LocoNetMessage(len); 295 // message exists, now fill it 296 msg.setOpCode(opCode); 297 msg.setElement(1, byte2); 298 log.trace("len: {}", len); // NOI18N 299 for (int i = 2; i < len; i++) { 300 // check for message-blocking error 301 int b = readByteProtected(istream) & 0xFF; 302 log.trace("char {} is: {}", i, Integer.toHexString(b)); // NOI18N 303 if ((b & 0x80) != 0) { 304 log.warn("LocoNet message with opCode: {} ended early. Expected length: {} seen length: {} unexpected byte: {}", Integer.toHexString(opCode), len, i, Integer.toHexString(b)); // NOI18N 305 opCode = b; 306 throw new LocoNetMessageException(); 307 } 308 msg.setElement(i, b); 309 } 310 } catch (LocoNetMessageException e) { 311 // retry by destroying the existing message 312 // opCode is set for the newly-started packet 313 msg = null; 314 } 315 } 316 // check parity 317 if (!msg.checkParity()) { 318 log.warn("Ignore LocoNet packet with bad checksum: {}", msg); 319 throw new LocoNetMessageException(); 320 } 321 // message is complete, dispatch it !! 322 log.trace("message complete: {}", msg); 323 324 // check if this message was supposed to be ignored 325 // sentList will be empty if preference "LoconetUpdateSlotOnMessageCreation" is not activated 326 if(trafficController.getSentList().contains(msg)) { 327 trafficController.getSentList().remove(msg); 328 log.trace("found packet {} in sentList, ignoring. {} packets in sentList remaining.", msg, trafficController.getSentList().size()); 329 } 330 else { 331 log.trace("queue message for notification: {}", msg); 332 jmri.util.ThreadingUtil.runOnLayoutEventually(new RcvMemo(msg, trafficController)); 333 } 334 335 // done with this one 336 } catch (LocoNetMessageException e) { 337 // just let it ride for now 338 log.warn("run: unexpected LocoNetMessageException", e); // NOI18N 339 } catch (java.io.InterruptedIOException e) { 340 // posted from idle port when enableReceiveTimeout used 341 // Normal condition, go around the loop again 342 } catch (java.io.IOException e) { 343 if (controller.getAllowConnectionRecovery()) { 344 log.info("run: server closed connection, attempting recovery"); 345 controller.closePort(); 346 controller.recover(); 347 } else { 348 // fired when read detects end-of-file 349 log.info("End of file", e); // NOI18N 350 dispose(); 351 disconnectPort(controller); 352 } 353 return; 354 } catch (RuntimeException e) { 355 // normally, we don't catch RuntimeException, but in this 356 // permanently running loop it seems wise. 357 log.warn("run: unexpected Exception", e); // NOI18N 358 } 359 } // end of permanent loop 360 } 361 } 362 363 /** 364 * Captive class to notify of one message. 365 */ 366 protected static class RcvMemo implements jmri.util.ThreadingUtil.ThreadAction { 367 368 public RcvMemo(LocoNetMessage msg, LnTrafficController trafficController) { 369 thisMsg = msg; 370 thisTc = trafficController; 371 } 372 LocoNetMessage thisMsg; 373 LnTrafficController thisTc; 374 375 /** 376 * {@inheritDoc} 377 */ 378 @Override 379 public void run() { 380 thisTc.notify(thisMsg); 381 } 382 } 383 384 /** 385 * Captive class to handle transmission. 386 */ 387 class XmtHandler implements Runnable { 388 389 /** 390 * Loops forever, looking for message to send and processing them. 391 */ 392 @Override 393 public void run() { 394 395 while (!threadStopRequest) { // loop until asked to stop 396 // any input? 397 try { 398 // get content; blocks until present 399 log.trace("check for input"); // NOI18N 400 401 byte[] msg = xmtList.take(); 402 403 // input - now send 404 try { 405 if (ostream != null) { 406 if (isXmtBusy()) { 407 log.debug("LocoNet port not ready to receive"); // NOI18N 408 } 409 log.trace("start write to stream: {}", jmri.util.StringUtil.hexStringFromBytes(msg)); // NOI18N 410 ostream.write(msg); 411 ostream.flush(); 412 log.trace("end write to stream: {}", jmri.util.StringUtil.hexStringFromBytes(msg)); // NOI18N 413 messageTransmitted(msg); 414 } else { 415 // no stream connected 416 log.warn("sendLocoNetMessage: no connection established"); // NOI18N 417 } 418 } catch (java.io.IOException e) { 419 log.warn("sendLocoNetMessage: IOException: {}", e.toString()); // NOI18N 420 421 if (controller.getAllowConnectionRecovery()) { 422 log.info("run: server closed connection, attempting recovery"); 423 controller.closePort(); 424 controller.recover(); 425 } 426 } 427 } catch (InterruptedException ie) { 428 return; // ending the thread 429 } catch (RuntimeException rt) { 430 log.error("Exception on take() call", rt); 431 } 432 } 433 } 434 } 435 436 /** 437 * When a message is finally transmitted, forward it to listeners if echoing 438 * is needed. 439 * 440 * @param msg message sent 441 */ 442 protected void messageTransmitted(byte[] msg) { 443 log.debug("message transmitted (echo {})", echo); 444 if (!echo) { 445 return; 446 } 447 448 LocoNetMessage m = new LocoNetMessage(msg); 449 450 // check if this message was supposed to be ignored 451 // sentList will be empty if preference "LoconetUpdateSlotOnMessageCreation" is not activated 452 if(getSentList().contains(m)) { 453 getSentList().remove(m); 454 log.trace("found packet {} in sentList, ignoring. {} packets in sentList remaining.", m, getSentList().size()); 455 } 456 else { 457 log.trace("queue message for notification: {}", m); 458 // message is queued for transmit, echo it when needed 459 // return a notification via the queue to ensure end 460 javax.swing.SwingUtilities.invokeLater(new Echo(this, m)); 461 } 462 } 463 464 static class Echo implements Runnable { 465 466 Echo(LnPacketizer t, LocoNetMessage m) { 467 myTc = t; 468 msgForLater = m; 469 } 470 LocoNetMessage msgForLater; 471 LnPacketizer myTc; 472 473 /** 474 * {@inheritDoc} 475 */ 476 @Override 477 public void run() { 478 myTc.notify(msgForLater); 479 } 480 } 481 482 /** 483 * Invoked at startup to start the threads needed here. 484 */ 485 public void startThreads() { 486 int priority = Thread.currentThread().getPriority(); 487 log.debug("startThreads current priority = {} max available = {} default = {} min available = {}", // NOI18N 488 priority, Thread.MAX_PRIORITY, Thread.NORM_PRIORITY, Thread.MIN_PRIORITY); 489 490 // start the RcvHandler in a thread of its own 491 if (rcvHandler == null) { 492 rcvHandler = new RcvHandler(this); 493 } 494 rcvThread = jmri.util.ThreadingUtil.newThread(rcvHandler, "LocoNet receive handler"); // NOI18N 495 rcvThread.setDaemon(true); 496 rcvThread.setPriority(Thread.MAX_PRIORITY); 497 rcvThread.start(); 498 499 if (xmtHandler == null) { 500 xmtHandler = new XmtHandler(); 501 } 502 // make sure that the xmt priority is no lower than the current priority 503 int xmtpriority = (Thread.MAX_PRIORITY - 1 > priority ? Thread.MAX_PRIORITY - 1 : Thread.MAX_PRIORITY); 504 // start the XmtHandler in a thread of its own 505 if (xmtThread == null) { 506 xmtThread = jmri.util.ThreadingUtil.newThread(xmtHandler, "LocoNet transmit handler"); // NOI18N 507 } 508 log.debug("Xmt thread starts at priority {}", xmtpriority); // NOI18N 509 xmtThread.setDaemon(true); 510 xmtThread.setPriority(Thread.MAX_PRIORITY - 1); 511 xmtThread.start(); 512 513 log.info("lnPacketizer Started"); 514 } 515 516 protected Thread rcvThread; 517 protected Thread xmtThread; 518 519 /** 520 * {@inheritDoc} 521 */ 522 // The join(150) is using a timeout because some receive threads 523 // (and maybe some day transmit threads) use calls that block 524 // even when interrupted. We wait 150 msec and proceed. 525 // Threads that do that are responsible for ending cleanly 526 // when the blocked call eventually returns. 527 @Override 528 public void dispose() { 529 threadStopRequest = true; 530 if (xmtThread != null) { 531 xmtThread.interrupt(); 532 try { 533 xmtThread.join(150); 534 } catch (InterruptedException e) { log.warn("unexpected InterruptedException", e);} 535 } 536 if (rcvThread != null) { 537 rcvThread.interrupt(); 538 try { 539 rcvThread.join(150); 540 } catch (InterruptedException e) { log.warn("unexpected InterruptedException", e);} 541 } 542 super.dispose(); 543 } 544 545 /** 546 * Terminate the receive and transmit threads. 547 * <p> 548 * This is intended to be used only by testing subclasses. 549 */ 550 // The join(150) is using a timeout because some receive threads 551 // (and maybe some day transmit threads) use calls that block 552 // even when interrupted. We wait 150 msec and proceed. 553 // Threads that do that are responsible for ending cleanly 554 // when the blocked call eventually returns. 555 public void terminateThreads() { 556 threadStopRequest = true; 557 if (xmtThread != null) { 558 xmtThread.interrupt(); 559 try { 560 xmtThread.join(150); 561 } catch (InterruptedException ie){ 562 // interrupted during cleanup. 563 } 564 } 565 566 if (rcvThread != null) { 567 rcvThread.interrupt(); 568 try { 569 rcvThread.join(150); 570 } catch (InterruptedException ie){ 571 // interrupted during cleanup. 572 } 573 } 574 } 575 576 /** 577 * Flag that threads should terminate as soon as they can. 578 */ 579 protected volatile boolean threadStopRequest = false; 580 581 private static final Logger log = LoggerFactory.getLogger(LnPacketizer.class); 582 583}