001package jmri.jmrix.dcc4pc; 002 003import java.io.DataInputStream; 004import java.lang.reflect.InvocationTargetException; 005import java.util.Calendar; 006 007import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 008 009import jmri.jmrix.AbstractMRListener; 010import jmri.jmrix.AbstractMRMessage; 011import jmri.jmrix.AbstractMRReply; 012import jmri.jmrix.AbstractMRTrafficController; 013import jmri.jmrix.dcc4pc.serialdriver.SerialDriverAdapter; 014 015import org.slf4j.Logger; 016import org.slf4j.LoggerFactory; 017 018/** 019 * Converts Stream-based I/O to/from DCC4PC messages. The "Dcc4PcInterface" side 020 * sends/receives message objects. 021 * <p> 022 * The connection to a Dcc4PcPortController is via a pair of *Streams, which 023 * then carry sequences of characters for transmission. Note that this 024 * processing is handled in an independent thread. 025 * <p> 026 * This handles the state transitions, based on the necessary state in each 027 * message. 028 * 029 * @author Bob Jacobsen Copyright (C) 2001 030 */ 031public class Dcc4PcTrafficController extends AbstractMRTrafficController implements Dcc4PcInterface { 032 033 /** 034 * Create a new DccPcTrafficController instance. 035 */ 036 public Dcc4PcTrafficController() { 037 super(); 038 if (log.isDebugEnabled()) { 039 log.debug("creating a new Dcc4PcTrafficController object"); 040 } 041 this.setAllowUnexpectedReply(false); 042 } 043 044 public void setAdapterMemo(Dcc4PcSystemConnectionMemo memo) { 045 adaptermemo = memo; 046 } 047 048 Dcc4PcSystemConnectionMemo adaptermemo; 049 050 @Override 051 public synchronized void addDcc4PcListener(Dcc4PcListener l) { 052 this.addListener(l); 053 } 054 055 @Override 056 public synchronized void removeDcc4PcListener(Dcc4PcListener l) { 057 this.removeListener(l); 058 } 059 060 /** Why this class locks the base class's xmtRunnable, which SpotBugs flags as visible outside the class. */ 061 private static final String XMT_LOCK = "The base class waits and notifies on xmtRunnable, so the receive" 062 + " and transmit threads must synchronise on that same object to wake each other"; 063 064 public static final int RETRIEVINGDATA = 100; 065 066 /** 067 * Forward a Dcc4PcMessage to all registered Dcc4PcInterface listeners. 068 */ 069 @Override 070 protected void forwardMessage(AbstractMRListener client, AbstractMRMessage m) { 071 ((Dcc4PcListener) client).message((Dcc4PcMessage) m); 072 } 073 074 /** 075 * Forward a Dcc4PcReply to all registered Dcc4PcInterface listeners. 076 */ 077 @Override 078 protected void forwardReply(AbstractMRListener client, AbstractMRReply r) { 079 ((Dcc4PcListener) client).reply((Dcc4PcReply) r); 080 } 081 082 @Override 083 protected AbstractMRMessage pollMessage() { 084 return null; 085 } 086 087 @Override 088 protected AbstractMRListener pollReplyHandler() { 089 return null; 090 } 091 092 /** 093 * Forward a preformatted message to the actual interface. 094 */ 095 @Override 096 public void sendDcc4PcMessage(Dcc4PcMessage m, Dcc4PcListener reply) { 097 sendMessage(m, reply); 098 } 099 100 protected boolean unsolicitedSensorMessageSeen = false; 101 102 //Dcc4Pc doesn't support this function. 103 @Override 104 protected AbstractMRMessage enterProgMode() { 105 return Dcc4PcMessage.getProgMode(); 106 } 107 108 // DCC4PC has no programming mode, so there's nothing to send. The old 109 // one-byte 0x00 message was the interface's own Get Info, and at shutdown 110 // terminate() sent it with 100 retries, flooding the log. 111 @Override 112 protected AbstractMRMessage enterNormalMode() { 113 return null; 114 } 115 116 /** 117 * At shutdown, put every board and the interface back to 8 kbps so other 118 * programs can talk to them. The next JMRI start resets the speed anyway. 119 */ 120 @Override 121 protected void terminate() { 122 log.debug("Cleanup Starts"); 123 stopJamWatchdog(); 124 if (ostream == null || port == null) { 125 return; // no connection established 126 } 127 // let a reply that is still being fetched finish first 128 long end = System.currentTimeMillis() + 1000; 129 while (sequenceInProgress && System.currentTimeMillis() < end) { 130 try { 131 Thread.sleep(5); 132 } catch (InterruptedException e) { 133 Thread.currentThread().interrupt(); 134 break; 135 } 136 } 137 Dcc4PcMessage m = Dcc4PcMessage.busResetSpeeds(); 138 m.setRetries(0); 139 // Don't wait on a write that will never finish: with the USB link jammed, 140 // waiting here would stop JMRI quitting. 141 boolean locked = false; 142 try { 143 locked = !xmtException && portLock.tryLock(1, java.util.concurrent.TimeUnit.SECONDS); 144 } catch (InterruptedException e) { 145 Thread.currentThread().interrupt(); 146 } 147 if (!locked) { 148 log.warn("DCC4PC interface not responding; bus speeds not reset at quit"); 149 return; 150 } 151 try { 152 forwardToPort(m, null); 153 } finally { 154 portLock.unlock(); 155 } 156 } 157 158 /** 159 * Held while writing to the port, by the transmit thread and by the 160 * receive thread (which sends Get Response itself), so the two never 161 * write at once. It's a separate lock, not this object's monitor, because 162 * a write can block for a long time when the interface's USB link jams; 163 * the GUI takes the monitor (to notify listeners and queue messages), and 164 * holding it during the write froze the whole window. 165 */ 166 private final java.util.concurrent.locks.ReentrantLock portLock = new java.util.concurrent.locks.ReentrantLock(); 167 168 /** When the current write started, or 0 if no write is in progress. */ 169 private volatile long writeStartedAt = 0; 170 171 /** A write that hasn't finished after this long means the USB link has jammed. */ 172 static final long STUCK_WRITE_MILLIS = 3000; 173 174 private volatile boolean jamReported = false; 175 private java.util.TimerTask jamWatchdog; 176 177 /** 178 * Watch for a write that never finishes. jSerialComm's write timeout 179 * doesn't always fire: on 2026-10-12 a write stayed blocked for over 10 180 * minutes, until the interface was unplugged. Call with portLock held. 181 */ 182 private void startJamWatchdog() { 183 if (jamWatchdog != null) { 184 return; 185 } 186 jamWatchdog = new java.util.TimerTask() { 187 @Override 188 public void run() { 189 long started = writeStartedAt; 190 if (started != 0 && System.currentTimeMillis() - started > STUCK_WRITE_MILLIS) { 191 reportJam("a write to the port has not finished after " + STUCK_WRITE_MILLIS / 1000 + " s"); 192 } 193 } 194 }; 195 jmri.util.TimerUtil.schedule(jamWatchdog, 1000, 1000); 196 } 197 198 private void stopJamWatchdog() { 199 java.util.TimerTask t = jamWatchdog; 200 if (t != null) { 201 t.cancel(); 202 } 203 } 204 205 /** 206 * {@inheritDoc} 207 */ 208 @Override 209 public void terminateThreads() { 210 stopJamWatchdog(); 211 Thread t = recoveryThread; 212 if (t != null) { 213 t.interrupt(); 214 } 215 super.terminateThreads(); 216 } 217 218 /** 219 * Tell the user, once per failure, that the interface has stopped 220 * responding and how to recover. Killing JMRI while a write is stuck in 221 * the USB driver has crashed Windows (bugcheck 0x1A), so the advice is to 222 * unplug first. 223 */ 224 private void reportJam(String why) { 225 if (jamReported) { 226 return; 227 } 228 jamReported = true; 229 String text = "The DCC4PC interface has stopped responding (" + why + ").\n" 230 + "Unplug its USB cable and plug it back in: JMRI will reconnect by itself.\n" 231 + "Don't end JMRI from Task Manager while the interface is still plugged in."; 232 log.error("The DCC4PC interface has stopped responding ({}). Unplug its USB cable and plug it back in:" 233 + " JMRI will reconnect by itself. Don't end JMRI from Task Manager while the interface is still plugged in.", why); 234 if (!java.awt.GraphicsEnvironment.isHeadless()) { 235 jmri.util.swing.JmriJOptionPane.showMessageDialogNonModal(null, text, 236 "DCC4PC interface not responding", jmri.util.swing.JmriJOptionPane.ERROR_MESSAGE, null); 237 } 238 } 239 240 /** 241 * Run on the layout thread once the port has been reopened after a 242 * failure. The interface restarts at 8 kbps when it is plugged back in, so 243 * the managers must set the bus up again as they do at startup. 244 */ 245 private volatile Runnable reconnectHandler; 246 247 public void setReconnectHandler(Runnable handler) { 248 reconnectHandler = handler; 249 } 250 251 /** True from a port failure until the port has been reopened. */ 252 volatile boolean recovering = false; 253 private volatile Thread recoveryThread; 254 private final Object recoveryLock = new Object(); 255 256 /** How often to check whether the port is back. */ 257 long recoveryPollMillis = 1000; 258 259 /** Longest wait between attempts to reopen a port that is listed but won't open. */ 260 static final long MAX_REOPEN_DELAY = 30000; 261 262 /** 263 * Reading or writing the port has failed, usually because the interface's 264 * USB link jammed and was then unplugged. Stop sending, tell the user, and 265 * reopen the port once it is back (issue 26). 266 */ 267 private void portFailed(String why) { 268 xmtException = true; 269 reportJam(why); 270 if (!(controller instanceof SerialDriverAdapter)) { 271 return; 272 } 273 synchronized (recoveryLock) { 274 if (recovering || threadStopRequest) { 275 return; 276 } 277 recovering = true; 278 Thread t = new Thread(() -> recoverPort((SerialDriverAdapter) controller), "DCC4PC port recovery"); 279 t.setDaemon(true); 280 recoveryThread = t; 281 t.start(); 282 } 283 } 284 285 /** 286 * Close the failed port, wait until the operating system lists it again 287 * (it disappears while the interface is unplugged), then reopen it. 288 * Runs on its own thread. 289 */ 290 private void recoverPort(SerialDriverAdapter adapter) { 291 String portName = adapter.getCurrentPortName(); 292 jmri.jmrix.ConnectionStatus.instance().setConnectionState(adapter.getSystemConnectionMemo(), 293 jmri.jmrix.ConnectionStatus.CONNECTION_DOWN); 294 log.info("Closing {}; it will be reopened when the interface is back", portName); 295 try { 296 closePort(adapter, portName); 297 boolean wasGone = false; 298 long delay = recoveryPollMillis; 299 while (!threadStopRequest) { 300 Thread.sleep(delay); 301 if (!adapter.isPortPresent()) { 302 if (!wasGone) { 303 log.info("{} has gone; waiting for the interface to be plugged back in", portName); 304 wasGone = true; 305 } 306 delay = recoveryPollMillis; 307 continue; 308 } 309 if (wasGone) { 310 // just plugged back in: give the system a moment to finish setting it up 311 Thread.sleep(2 * recoveryPollMillis); 312 wasGone = false; 313 } 314 String error = adapter.openPort(portName, "JMRI"); 315 if (error == null) { 316 reattach(adapter, portName); 317 return; 318 } 319 delay = Math.min(Math.max(delay, recoveryPollMillis) * 2, MAX_REOPEN_DELAY); 320 log.warn("Could not reopen {} ({}); trying again in {} s", portName, error, delay / 1000); 321 } 322 } catch (InterruptedException e) { 323 Thread.currentThread().interrupt(); 324 log.debug("Port recovery stopped"); 325 } 326 } 327 328 /** 329 * Close the port on a separate thread, in case the driver blocks: a write 330 * stuck in a jammed USB driver has held on to the port before. 331 */ 332 private void closePort(SerialDriverAdapter adapter, String portName) throws InterruptedException { 333 Thread closer = new Thread(adapter::closeSerialPort, "DCC4PC port close"); 334 closer.setDaemon(true); 335 closer.start(); 336 closer.join(2000); 337 if (closer.isAlive()) { 338 log.warn("{} did not close within 2 s; trying to reopen it anyway", portName); 339 } 340 } 341 342 /** Switch to the reopened port's streams, and start sending again. */ 343 private void reattach(SerialDriverAdapter adapter, String portName) throws InterruptedException { 344 portLock.lockInterruptibly(); // a write may still be finishing on the old port 345 try { 346 istream = adapter.getInputStream(); 347 ostream = adapter.getOutputStream(); 348 port = adapter.getSerialPort(); 349 lastIncomplete = null; 350 waitingForMore = false; 351 sequenceInProgress = false; 352 mLastMessage = null; 353 mLastSentMessage = null; 354 rcvException = false; 355 xmtException = false; 356 jamReported = false; 357 } finally { 358 portLock.unlock(); 359 } 360 // whatever was queued during the outage is stale; the managers start again from scratch 361 synchronized (this) { 362 msgQueue.clear(); 363 listenerQueue.clear(); 364 } 365 recovering = false; 366 jmri.jmrix.ConnectionStatus.instance().setConnectionState(adapter.getSystemConnectionMemo(), 367 jmri.jmrix.ConnectionStatus.CONNECTION_UP); 368 log.info("Reopened {}; setting up the DCC4PC bus again", portName); 369 Runnable handler = reconnectHandler; 370 if (handler != null) { 371 jmri.util.ThreadingUtil.runOnLayoutEventually(handler::run); 372 } 373 } 374 375 @Override 376 protected void addTrailerToOutput(byte[] msg, int offset, AbstractMRMessage m) { 377 } 378 379 Dcc4PcMessage mLastMessage; //Last message requested with a reply listener ie from external methods 380 Dcc4PcMessage mLastSentMessage; //Last message actually sent from within the code, ie getResponse. 381 382 // Not synchronized on this object: see portLock. 383 @Override 384 protected void forwardToPort(AbstractMRMessage m, AbstractMRListener reply) { 385 if (log.isDebugEnabled()) { 386 log.debug("forwardToPort message: [{}]", m); 387 } 388 if (port == null) { 389 return; 390 } 391 portLock.lock(); 392 try { 393 // remember who sent this 394 mLastSender = reply; 395 mLastMessage = (Dcc4PcMessage) m; 396 397 // forward the message to the registered recipients, 398 // which includes the communications monitor, except the sender. 399 // Schedule notification via the Swing event queue to ensure order 400 if (!mLastMessage.isGetResponse()) { 401 //Do not forward on the get response packets, saves filling up the monitors with chaff 402 Runnable r = new XmtNotifier(m, mLastSender, this); 403 javax.swing.SwingUtilities.invokeLater(r); 404 } 405 forwardToPort(m); 406 } finally { 407 portLock.unlock(); 408 } 409 } 410 411 //this forward to port is also used internally for repeating commands. 412 private void forwardToPort(AbstractMRMessage m) { 413 portLock.lock(); 414 try { 415 writeToPort(m); 416 } finally { 417 portLock.unlock(); 418 } 419 } 420 421 // Call with portLock held. 422 @SuppressFBWarnings(value = "USO_UNSAFE_OBJECT_SYNCHRONIZATION", justification = XMT_LOCK) 423 private void writeToPort(AbstractMRMessage m) { 424 if (xmtException) { 425 // A write has failed and the port is being reopened, so the retry 426 // loop below would only wait out each message's timeouts. 427 return; 428 } 429 startJamWatchdog(); 430 mLastSentMessage = (Dcc4PcMessage) m; 431 // stream to port in single write, as that's needed by serial 432 byte msg[] = new byte[lengthOfByteStream(m)]; 433 434 // add data content 435 int len = m.getNumDataElements(); 436 for (int i = 0; i < len; i++) { 437 msg[i] = (byte) m.getElement(i); 438 } 439 440 try { 441 if (ostream != null) { 442 if (log.isDebugEnabled()) { 443 StringBuilder f = new StringBuilder(); 444 for (int i = 0; i < msg.length; i++) { 445 f.append(Integer.toHexString(0xFF & msg[i])); 446 f.append(" "); 447 } 448 log.debug("formatted message: {}", f); 449 } 450 while (m.getRetries() >= 0) { 451 if (portReadyToSend(controller)) { 452 // DSR high means the interface is still answering the last command. 453 // (It also reads high just after the port opens, so don't wait long.) 454 waitForDsrLow(200, false); 455 waitForGap(); 456 port.setDTR(true); 457 writeStartedAt = System.currentTimeMillis(); 458 try { 459 ostream.write(msg); 460 } finally { 461 writeStartedAt = 0; 462 } 463 try { 464 Thread.sleep(20); 465 } catch (InterruptedException ex) { 466 Thread.currentThread().interrupt(); 467 } catch (Exception ex) { 468 log.warn("sendMessage: Exception: {}", ex.toString()); 469 } 470 ostream.flush(); 471 port.setDTR(false); 472 if (((Dcc4PcMessage) m).isNoDataReply()) { 473 waitForDsrLow(m.getTimeout(), true); 474 lastTransactionEnd = System.currentTimeMillis(); 475 } 476 break; 477 } else if (m.getRetries() >= 0) { 478 log.debug("Retry message: {} attempts remaining: {}", m, m.getRetries()); 479 m.setRetries(m.getRetries() - 1); 480 try { 481 synchronized (xmtRunnable) { 482 xmtRunnable.wait(m.getTimeout()); 483 } 484 } catch (InterruptedException e) { 485 Thread.currentThread().interrupt(); // retain if needed later 486 log.error("retry wait interrupted"); 487 } 488 } else { 489 log.warn("sendMessage: port not ready for data sending: {}", java.util.Arrays.toString(msg)); 490 } 491 } 492 } else { 493 // ostream is null 494 // no stream connected 495 connectionWarn(); 496 } 497 } catch (java.io.IOException | RuntimeException e) { 498 portWarn(e); 499 portFailed("a write to the port failed: " + e.getMessage()); 500 } 501 } 502 /** 503 * Wait for the interface to finish a command that it answers with no data: 504 * DSR falls when it is done, usually within 2 ms of DTR going low. 505 * 506 * @param timeout longest wait, in milliseconds 507 * @param warn log a warning if DSR is still high at the end 508 */ 509 private void waitForDsrLow(int timeout, boolean warn) { 510 long end = System.currentTimeMillis() + timeout; 511 while (port.isDSR()) { 512 if (System.currentTimeMillis() > end) { 513 if (warn) { 514 log.warn("Interface still busy (DSR high) {} ms after {}", timeout, mLastSentMessage); 515 } else { 516 log.debug("DSR still high after {} ms, sending anyway", timeout); 517 } 518 return; 519 } 520 try { 521 Thread.sleep(1); 522 } catch (InterruptedException ex) { 523 Thread.currentThread().interrupt(); 524 return; 525 } 526 } 527 } 528 529 /** 530 * Quiet time between the end of one transaction and the start of the 531 * next. Sending each Get Response straight after the last reply ended 532 * jammed the interface's USB link after a few minutes of polling with two 533 * locos; our standalone tools always leave about this long, and ran for 534 * minutes without a jam. 535 */ 536 static final int MIN_GAP_MS = 20; 537 538 volatile long lastTransactionEnd = 0; 539 540 private void waitForGap() { 541 long wait = lastTransactionEnd + MIN_GAP_MS - System.currentTimeMillis(); 542 if (wait > 0) { 543 try { 544 Thread.sleep(wait); 545 } catch (InterruptedException ex) { 546 Thread.currentThread().interrupt(); 547 } 548 } 549 } 550 551 volatile jmri.jmrix.purejavacomm.SerialPort port; 552 553 @Override 554 public void connectPort(jmri.jmrix.AbstractPortController p) { 555 556 super.connectPort(p); 557 port = ((SerialDriverAdapter) controller).getSerialPort(); 558 559 } 560 561 /** 562 * Like the base class's loop, but a failed port doesn't end it: the port 563 * is reopened (see {@link #portFailed}) and reading carries on. 564 */ 565 @Override 566 public void receiveLoop() { 567 log.debug("receiveLoop starts in {}", this); 568 int errorCount = 0; 569 while (!threadStopRequest) { 570 if (recovering) { 571 try { 572 Thread.sleep(50); 573 } catch (InterruptedException e) { 574 break; 575 } 576 continue; 577 } 578 try { 579 handleOneIncomingReply(); 580 errorCount = 0; 581 } catch (java.io.InterruptedIOException e) { 582 if (!recovering) { 583 break; // asked to stop 584 } 585 } catch (java.io.IOException e) { 586 if (!threadStopRequest) { 587 portFailed("reading from the port failed: " + e.getMessage()); 588 } 589 } catch (RuntimeException e) { 590 log.error("Exception in receive loop: {}", e.toString(), e); 591 if (++errorCount >= maxRcvExceptionCount) { 592 errorCount = 0; 593 portFailed("too many errors reading from the port"); 594 } 595 } 596 } 597 log.debug("receiveLoop ends"); 598 } 599 600 @Override 601 protected AbstractMRReply newReply() { 602 Dcc4PcReply reply = new Dcc4PcReply(); 603 return reply; 604 } 605 606 // for now, receive always OK 607 @Override 608 protected boolean canReceive() { 609 return true; 610 } 611 612 @Override 613 protected boolean endOfMessage(AbstractMRReply msg) { 614 if (port.isDSR()) { 615 return false; 616 } 617 try { 618 if (controller.getInputStream().available() > 0) { 619 if (port.isRI()) { 620 log.debug("??? Ringing true ???"); 621 } 622 return false; 623 } 624 625 //log.debug("No more input available " + port.isDSR()); 626 if (port.isRI()) { 627 log.debug("??? Ringing true ???"); 628 } 629 return true; 630 } catch (java.io.IOException ex) { 631 log.error("IO Exception{}", ex.toString()); 632 } 633 return !port.isDSR(); 634 } 635 636 @Override 637 protected void handleTimeout(AbstractMRMessage msg, AbstractMRListener l) { 638 if(l != null){ 639 ((Dcc4PcListener) l).handleTimeout((Dcc4PcMessage) msg); 640 } 641 super.handleTimeout(msg, l); 642 } 643 644 Dcc4PcReply lastIncomplete; 645 boolean waitingForMore = false; 646 boolean loading = false; 647 648 // True while the receive thread is fetching a reply with Get Response; the 649 // transmit thread must not send another command until it has finished. 650 volatile boolean sequenceInProgress = false; 651 652 /** Longest a single reply sequence may hold up the transmit thread. */ 653 static final int MAX_SEQUENCE_TIME = 10000; 654 655 final int GETMOREDATA = 0x01; 656 657 /** 658 * Handle each reply when complete. 659 * <p> 660 * (This is public for testing purposes) Runs in the "Receive" thread. 661 * 662 */ 663 @Override 664 @SuppressFBWarnings(value = "USO_UNSAFE_OBJECT_SYNCHRONIZATION", justification = XMT_LOCK) 665 public void handleOneIncomingReply() throws java.io.IOException { 666 // we sit in this until the message is complete, relying on 667 // threading to let other stuff happen 668 669 // Create message off the right concrete class 670 AbstractMRReply msg = newReply(); 671 672 // message exists, now fill it 673 loadChars(msg, istream); 674 if (mLastSentMessage != null) { 675 ((Dcc4PcReply)msg).setOriginalRequest(mLastMessage); 676 //log.debug(mLastMessage.getElement(0)); 677 if (mLastSentMessage.isForChildBoard()) { 678 if (log.isDebugEnabled()) { 679 log.debug("This is a message for a child board {}", ((Dcc4PcReply) msg).toHexString()); 680 log.debug("Originate {}", mLastMessage.toString()); 681 } 682 if ((mLastSentMessage.getNumDataElements() - 1) == msg.getElement(1)) { 683 log.debug("message lengths match"); 684 waitingForMore = true; 685 try { 686 Thread.sleep(10); 687 } catch (InterruptedException ex) { 688 log.debug("InterruptedException", ex); 689 } 690 //log.debug("We do not forward the response to the listener as it has not been formed"); 691 lastIncomplete = null; 692 sequenceInProgress = true; 693 forwardToPort(Dcc4PcMessage.getResponse()); 694 695 return; 696 } else { 697 if (log.isDebugEnabled()) { 698 log.debug("Not all of the command was sent, we need to figure out a way to resend the bits"); 699 log.debug("Original Message length {}", mLastSentMessage.getNumDataElements()); 700 log.debug("What CID has procced in size {}", (byte) msg.getElement(1)); 701 log.debug("Reply is in error {}", ((Dcc4PcReply) msg).toHexString()); 702 } 703 } 704 } else if (mLastSentMessage.getElement(0) == 0x0C) { 705 if (log.isDebugEnabled()) { 706 log.debug("last message was a get response {}", ((Dcc4PcReply) msg).toHexString()); 707 } 708 if (msg.getElement(0) == Dcc4PcReply.SUCCESS) { 709 ((Dcc4PcReply) msg).strip(); 710 if (lastIncomplete != null) { 711 //log.debug("Need to add the new reply to this message"); 712 //log.debug("existing : " + lastIncomplete.toHexString()); 713 714 //Append this message to the last incomplete message 715 if (msg.getNumDataElements() != 0) { 716 int iOrig = lastIncomplete.getNumDataElements(); 717 int iNew = 0; 718 while (iNew < msg.getNumDataElements()) { 719 lastIncomplete.setElement(iOrig, msg.getElement(iNew)); 720 iOrig++; 721 iNew++; 722 } 723 } 724 //set the last incomplete message as the one to return 725 log.debug("Reply set as lastIncomplete"); 726 msg = lastIncomplete; 727 } 728 ((Dcc4PcReply) msg).setError(false); 729 ((Dcc4PcReply)msg).setOriginalRequest(mLastMessage); 730 lastIncomplete = null; 731 waitingForMore = false; 732 mLastMessage = null; 733 mLastSentMessage = null; 734 } else if (msg.getElement(0) == Dcc4PcReply.INCOMPLETE) { 735 waitingForMore = true; 736 ((Dcc4PcReply) msg).strip(); 737 if (lastIncomplete != null) { 738 //Append this message to the last incomplete message 739 if (msg.getNumDataElements() != 0) { 740 int iOrig = lastIncomplete.getNumDataElements(); 741 int iNew = 0; 742 while (iNew < msg.getNumDataElements()) { 743 lastIncomplete.setElement(iOrig, msg.getElement(iNew)); 744 iOrig++; 745 iNew++; 746 } 747 } 748 749 } else if (msg.getNumDataElements() > 0) { // a first chunk of one byte is data too 750 lastIncomplete = (Dcc4PcReply) msg; 751 } 752 //We do not forward the response to the listener as it has not been formed 753 sequenceInProgress = true; 754 forwardToPort(Dcc4PcMessage.getResponse()); 755 756 return; 757 758 } else { 759 log.debug("Reply is an error mesage"); 760 ((Dcc4PcReply) msg).setError(true); 761 mLastMessage.setRetries(mLastMessage.getRetries() - 1); 762 if (mLastMessage.getRetries() >= 0) { 763 sequenceInProgress = false; 764 synchronized (xmtRunnable) { 765 mCurrentState = AUTORETRYSTATE; 766 replyInDispatch = false; 767 xmtRunnable.notify(); 768 } 769 return; 770 } 771 } 772 } 773 } else { 774 log.debug("Last message sent was null {}", ((Dcc4PcReply) msg).toHexString()); 775 } 776 777 // message is complete, dispatch it !! 778 sequenceInProgress = false; 779 replyInDispatch = true; 780 if (log.isDebugEnabled()) { 781 log.debug("dispatch reply of length {} contains {} state {}", msg.getNumDataElements(), msg.toString(), mCurrentState); 782 } 783 // forward the message to the registered recipients, 784 // which includes the communications monitor 785 // return a notification via the Swing event queue to ensure proper thread 786 Runnable r = newRcvNotifier(msg, mLastSender, this); 787 try { 788 javax.swing.SwingUtilities.invokeAndWait(r); 789 } catch (InterruptedException | InvocationTargetException e) { 790 log.error("Unexpected exception in invokeAndWait:", e); 791 } 792 793 if (log.isDebugEnabled()) { 794 log.debug("dispatch thread invoked"); 795 } 796 if (!msg.isUnsolicited()) { 797 // effect on transmit: 798 switch (mCurrentState) { 799 case WAITMSGREPLYSTATE: { 800 // check to see if the response was an error message we want 801 // to automatically handle by re-queueing the last sent 802 // message, otherwise go on to the next message 803 if (msg.isRetransmittableErrorMsg()) { 804 if (log.isDebugEnabled()) { 805 log.debug("Automatic Recovery from Error Message: {}", msg.toString()); 806 } 807 synchronized (xmtRunnable) { 808 mCurrentState = AUTORETRYSTATE; 809 replyInDispatch = false; 810 xmtRunnable.notify(); 811 } 812 } else { 813 // update state, and notify to continue 814 synchronized (xmtRunnable) { 815 mCurrentState = NOTIFIEDSTATE; 816 replyInDispatch = false; 817 xmtRunnable.notify(); 818 } 819 } 820 break; 821 } 822 case WAITREPLYINPROGMODESTATE: { 823 // entering programming mode 824 mCurrentMode = PROGRAMINGMODE; 825 replyInDispatch = false; 826 827 // check to see if we need to delay to allow decoders to become 828 // responsive 829 int warmUpDelay = enterProgModeDelayTime(); 830 if (warmUpDelay != 0) { 831 try { 832 synchronized (xmtRunnable) { 833 xmtRunnable.wait(warmUpDelay); 834 } 835 } catch (InterruptedException e) { 836 Thread.currentThread().interrupt(); // retain if needed later 837 } 838 } 839 // update state, and notify to continue 840 synchronized (xmtRunnable) { 841 mCurrentState = OKSENDMSGSTATE; 842 xmtRunnable.notify(); 843 } 844 break; 845 } 846 case WAITREPLYINNORMMODESTATE: { 847 // entering normal mode 848 mCurrentMode = NORMALMODE; 849 replyInDispatch = false; 850 // update state, and notify to continue 851 synchronized (xmtRunnable) { 852 mCurrentState = OKSENDMSGSTATE; 853 xmtRunnable.notify(); 854 } 855 break; 856 } 857 default: { 858 replyInDispatch = false; 859 unexpectedReplyStateError(mCurrentState,msg.toString()); 860 } 861 } 862 // Unsolicited message 863 } else { 864 if (log.isDebugEnabled()) { 865 log.debug("Unsolicited Message Received {}", msg.toString()); 866 } 867 replyInDispatch = false; 868 } 869 } 870 871 boolean normalFlushReceiveChars = false; 872 873 //Need a way to detect that the dsr has gone low. 874 @Override 875 protected void loadChars(AbstractMRReply msg, DataInputStream istream) 876 throws java.io.IOException { 877 int i; 878 readingData = false; 879 MAINGET: 880 { 881 for (i = 0; i < msg.maxSize(); i++) { 882 boolean waiting = true; 883 while (waiting) { 884 if (recovering) { 885 // the port is closed or about to be; the receive loop waits for it to reopen 886 throw new java.io.InterruptedIOException("port being reopened"); 887 } 888 if (controller.getInputStream().available() > 0) { 889 readingData = true; 890 byte char1 = readByteProtected(istream); 891 waiting = false; 892 893 //potentially add in a flush here that is generated by the transmit after a command has been sent, but this is not an error type flush.l 894 // if there was a timeout, flush any char received and start over 895 if (flushReceiveChars) { 896 lastIncomplete = null; 897 waitingForMore = false; 898 sequenceInProgress = false; 899 mLastMessage = null; 900 mLastSentMessage = null; 901 readingData = false; 902 log.warn("timeout flushes receive buffer: {}", ((Dcc4PcReply) msg).toHexString()); 903 msg.flush(); 904 i = 0; // restart 905 flushReceiveChars = false; 906 waiting = true; 907 } else { 908 if (canReceive()) { 909 if (log.isDebugEnabled()) { 910 log.debug("Set data {}, {}", i, char1 & 0xff); 911 } 912 msg.setElement(i, char1); 913 waiting = false; 914 if (port.isRI()) { 915 log.debug("Ring high error"); 916 ((Dcc4PcReply) msg).setError(true); 917 break MAINGET; 918 } 919 if (endOfMessage(msg)) { 920 break MAINGET; 921 } 922 } else { 923 i--; // flush char 924 log.error("unsolicited character received: {}", Integer.toHexString(char1)); 925 } 926 } 927 } else if (!port.isDSR()) { 928 if (i == 0) { 929 waiting = true; 930 idleWait(); 931 } else { 932 log.debug("We have data so will break"); 933 waiting = false; 934 break MAINGET; 935 } 936 } else { 937 //As we have no data to process we will set the readingData flag false; 938 readingData = false; 939 // Only pause before the reply starts. Once its bytes are arriving, read 940 // each one as soon as it's there: pausing mid-reply read the bytes late, 941 // and was the likely cause of replies cut short (issue 29). 942 if (i == 0) { 943 idleWait(); 944 } 945 } 946 } 947 } 948 } 949 lastTransactionEnd = System.currentTimeMillis(); 950 } 951 952 /** 953 * Pause briefly while loadChars is waiting for a reply to start. Without 954 * it the receive thread polled available() and DSR non-stop, using a whole 955 * CPU core whenever JMRI was idle (issue 29). It isn't used once a reply's 956 * bytes are arriving: on Windows a 1 ms sleep can last up to ~15 ms, and 957 * with it the Raw stream misaligned (7 times in one 5-minute run, against 958 * none without it). 959 * 960 * @throws java.io.InterruptedIOException if the thread is interrupted or 961 * asked to stop, which ends the 962 * receive loop 963 */ 964 private void idleWait() throws java.io.InterruptedIOException { 965 if (threadStopRequest) { 966 throw new java.io.InterruptedIOException("receive thread stopping"); 967 } 968 try { 969 Thread.sleep(1); 970 } catch (InterruptedException ex) { 971 Thread.currentThread().interrupt(); 972 throw new java.io.InterruptedIOException("receive thread interrupted"); 973 } 974 } 975 976 boolean readingData = false; 977 978 @Override 979 @SuppressFBWarnings(value = "USO_UNSAFE_OBJECT_SYNCHRONIZATION", justification = XMT_LOCK) 980 protected void transmitWait(int waitTime, int state, String InterruptMessage) { 981 // wait() can have spurious wakeup! 982 // so we protect by making sure the entire timeout time is used 983 long currentTime = Calendar.getInstance().getTimeInMillis(); 984 long endTime = currentTime + waitTime; 985 long hardEnd = currentTime + Math.max(waitTime, MAX_SEQUENCE_TIME); 986 while (endTime > (currentTime = Calendar.getInstance().getTimeInMillis())) { 987 long wait = endTime - currentTime; 988 try { 989 synchronized (xmtRunnable) { 990 // Do not wait if the current state has changed since we 991 // last set it. 992 993 if (mCurrentState != state) { 994 return; 995 } 996 xmtRunnable.wait(wait); // rcvr normally ends this w state change 997 //If we are in the process of reading the data then do not time out. 998 if (readingData) { 999 endTime = endTime + 10; 1000 } 1001 //if we have received a packet and a seperate message has been sent to retrieve 1002 //the reply we will add more time to our wait process. 1003 if (waitingForMore) { 1004 waitingForMore = false; 1005 //if we are in the process of retrieving data, then we shall increase the endTime by 200ms. 1006 endTime = endTime + 200; 1007 } 1008 // Timing out part way through a Get Response sequence would let the next 1009 // command interrupt it, which can jam the interface's USB link. 1010 long now = Calendar.getInstance().getTimeInMillis(); 1011 if (sequenceInProgress && endTime <= now + 50 && now < hardEnd) { 1012 endTime = now + 50; 1013 } 1014 } 1015 } catch (InterruptedException e) { 1016 Thread.currentThread().interrupt(); // retain if needed later 1017 log.error("{} from {}", InterruptMessage, e.getMessage()); 1018 } 1019 } 1020 log.debug("TIMEOUT in transmitWait, mCurrentState:{} {} port dsr {} wait time {}", mCurrentState, state, port.isDSR(), waitTime); 1021 } 1022 1023 private static final Logger log = LoggerFactory.getLogger(Dcc4PcTrafficController.class); 1024}