001package jmri.jmrix.dcc4pc; 002 003import java.io.DataInputStream; 004import java.lang.reflect.InvocationTargetException; 005import java.util.Calendar; 006import jmri.jmrix.AbstractMRListener; 007import jmri.jmrix.AbstractMRMessage; 008import jmri.jmrix.AbstractMRReply; 009import jmri.jmrix.AbstractMRTrafficController; 010import jmri.jmrix.dcc4pc.serialdriver.SerialDriverAdapter; 011import org.slf4j.Logger; 012import org.slf4j.LoggerFactory; 013import purejavacomm.SerialPort; 014 015/** 016 * Converts Stream-based I/O to/from DCC4PC messages. The "Dcc4PcInterface" side 017 * sends/receives message objects. 018 * <p> 019 * The connection to a Dcc4PcPortController is via a pair of *Streams, which 020 * then carry sequences of characters for transmission. Note that this 021 * processing is handled in an independent thread. 022 * <p> 023 * This handles the state transitions, based on the necessary state in each 024 * message. 025 * 026 * @author Bob Jacobsen Copyright (C) 2001 027 */ 028public class Dcc4PcTrafficController extends AbstractMRTrafficController implements Dcc4PcInterface { 029 030 /** 031 * Create a new DccPcTrafficController instance. 032 */ 033 public Dcc4PcTrafficController() { 034 super(); 035 if (log.isDebugEnabled()) { 036 log.debug("creating a new Dcc4PcTrafficController object"); 037 } 038 this.setAllowUnexpectedReply(false); 039 } 040 041 public void setAdapterMemo(Dcc4PcSystemConnectionMemo memo) { 042 adaptermemo = memo; 043 } 044 045 Dcc4PcSystemConnectionMemo adaptermemo; 046 047 @Override 048 public synchronized void addDcc4PcListener(Dcc4PcListener l) { 049 this.addListener(l); 050 } 051 052 @Override 053 public synchronized void removeDcc4PcListener(Dcc4PcListener l) { 054 this.removeListener(l); 055 } 056 057 public static final int RETRIEVINGDATA = 100; 058 059 /** 060 * Forward a Dcc4PcMessage to all registered Dcc4PcInterface listeners. 061 */ 062 @Override 063 protected void forwardMessage(AbstractMRListener client, AbstractMRMessage m) { 064 ((Dcc4PcListener) client).message((Dcc4PcMessage) m); 065 } 066 067 /** 068 * Forward a Dcc4PcReply to all registered Dcc4PcInterface listeners. 069 */ 070 @Override 071 protected void forwardReply(AbstractMRListener client, AbstractMRReply r) { 072 ((Dcc4PcListener) client).reply((Dcc4PcReply) r); 073 } 074 075 @Override 076 protected AbstractMRMessage pollMessage() { 077 return null; 078 } 079 080 @Override 081 protected AbstractMRListener pollReplyHandler() { 082 return null; 083 } 084 085 /** 086 * Forward a preformatted message to the actual interface. 087 */ 088 @Override 089 public void sendDcc4PcMessage(Dcc4PcMessage m, Dcc4PcListener reply) { 090 sendMessage(m, reply); 091 } 092 093 protected boolean unsolicitedSensorMessageSeen = false; 094 095 //Dcc4Pc doesn't support this function. 096 @Override 097 protected AbstractMRMessage enterProgMode() { 098 return Dcc4PcMessage.getProgMode(); 099 } 100 101 //Dcc4Pc doesn't support this function! 102 @Override 103 protected AbstractMRMessage enterNormalMode() { 104 return Dcc4PcMessage.getExitProgMode(); 105 } 106 107 @Override 108 protected void addTrailerToOutput(byte[] msg, int offset, AbstractMRMessage m) { 109 } 110 111 Dcc4PcMessage mLastMessage; //Last message requested with a reply listener ie from external methods 112 Dcc4PcMessage mLastSentMessage; //Last message actually sent from within the code, ie getResponse. 113 114 @Override 115 synchronized protected void forwardToPort(AbstractMRMessage m, AbstractMRListener reply) { 116 if (log.isDebugEnabled()) { 117 log.debug("forwardToPort message: [{}]", m); 118 } 119 if (port == null) { 120 return; 121 } 122 // remember who sent this 123 mLastSender = reply; 124 mLastMessage = (Dcc4PcMessage) m; 125 126 // forward the message to the registered recipients, 127 // which includes the communications monitor, except the sender. 128 // Schedule notification via the Swing event queue to ensure order 129 if (!mLastMessage.isGetResponse()) { 130 //Do not forward on the get response packets, saves filling up the monitors with chaff 131 Runnable r = new XmtNotifier(m, mLastSender, this); 132 javax.swing.SwingUtilities.invokeLater(r); 133 } 134 forwardToPort(m); 135 136 } 137 138 //this forward to port is also used internally for repeating commands. 139 private void forwardToPort(AbstractMRMessage m) { 140 mLastSentMessage = (Dcc4PcMessage) m; 141 // stream to port in single write, as that's needed by serial 142 byte msg[] = new byte[lengthOfByteStream(m)]; 143 144 // add data content 145 int len = m.getNumDataElements(); 146 for (int i = 0; i < len; i++) { 147 msg[i] = (byte) m.getElement(i); 148 } 149 150 try { 151 if (ostream != null) { 152 if (log.isDebugEnabled()) { 153 StringBuilder f = new StringBuilder(); 154 for (int i = 0; i < msg.length; i++) { 155 f.append(Integer.toHexString(0xFF & msg[i])); 156 f.append(" "); 157 } 158 log.debug("formatted message: {}", f); 159 } 160 while (m.getRetries() >= 0) { 161 if (portReadyToSend(controller)) { 162 port.setDTR(true); 163 ostream.write(msg); 164 try { 165 Thread.sleep(20); 166 } catch (InterruptedException ex) { 167 Thread.currentThread().interrupt(); 168 } catch (Exception ex) { 169 log.warn("sendMessage: Exception: {}", ex.toString()); 170 } 171 ostream.flush(); 172 port.setDTR(false); 173 break; 174 } else if (m.getRetries() >= 0) { 175 log.debug("Retry message: {} attempts remaining: {}", m, m.getRetries()); 176 m.setRetries(m.getRetries() - 1); 177 try { 178 synchronized (xmtRunnable) { 179 xmtRunnable.wait(m.getTimeout()); 180 } 181 } catch (InterruptedException e) { 182 Thread.currentThread().interrupt(); // retain if needed later 183 log.error("retry wait interrupted"); 184 } 185 } else { 186 log.warn("sendMessage: port not ready for data sending: {}", java.util.Arrays.toString(msg)); 187 } 188 } 189 } else { 190 // ostream is null 191 // no stream connected 192 connectionWarn(); 193 } 194 } catch (java.io.IOException | RuntimeException e) { 195 // TODO Currently there's no port recovery if an exception occurs 196 // must restart JMRI to clear xmtException. 197 xmtException = true; 198 portWarn(e); 199 } 200 } 201 SerialPort port; 202 203 @Override 204 public void connectPort(jmri.jmrix.AbstractPortController p) { 205 206 super.connectPort(p); 207 port = ((SerialDriverAdapter) controller).getSerialPort(); 208 209 } 210 211 @Override 212 protected AbstractMRReply newReply() { 213 Dcc4PcReply reply = new Dcc4PcReply(); 214 return reply; 215 } 216 217 // for now, receive always OK 218 @Override 219 protected boolean canReceive() { 220 return true; 221 } 222 223 @Override 224 protected boolean endOfMessage(AbstractMRReply msg) { 225 if (port.isDSR()) { 226 return false; 227 } 228 try { 229 if (controller.getInputStream().available() > 0) { 230 if (port.isRI()) { 231 log.debug("??? Ringing true ???"); 232 } 233 return false; 234 } 235 236 //log.debug("No more input available " + port.isDSR()); 237 if (port.isRI()) { 238 log.debug("??? Ringing true ???"); 239 } 240 return true; 241 } catch (java.io.IOException ex) { 242 log.error("IO Exception{}", ex.toString()); 243 } 244 return !port.isDSR(); 245 } 246 247 @Override 248 protected void handleTimeout(AbstractMRMessage msg, AbstractMRListener l) { 249 if(l != null){ 250 ((Dcc4PcListener) l).handleTimeout((Dcc4PcMessage) msg); 251 } 252 super.handleTimeout(msg, l); 253 } 254 255 Dcc4PcReply lastIncomplete; 256 boolean waitingForMore = false; 257 boolean loading = false; 258 259 final int GETMOREDATA = 0x01; 260 261 /** 262 * Handle each reply when complete. 263 * <p> 264 * (This is public for testing purposes) Runs in the "Receive" thread. 265 * 266 */ 267 @Override 268 public void handleOneIncomingReply() throws java.io.IOException { 269 // we sit in this until the message is complete, relying on 270 // threading to let other stuff happen 271 272 // Create message off the right concrete class 273 AbstractMRReply msg = newReply(); 274 275 // message exists, now fill it 276 loadChars(msg, istream); 277 if (mLastSentMessage != null) { 278 ((Dcc4PcReply)msg).setOriginalRequest(mLastMessage); 279 //log.debug(mLastMessage.getElement(0)); 280 if (mLastSentMessage.isForChildBoard()) { 281 if (log.isDebugEnabled()) { 282 log.debug("This is a message for a child board {}", ((Dcc4PcReply) msg).toHexString()); 283 log.debug("Originate {}", mLastMessage.toString()); 284 } 285 if ((mLastSentMessage.getNumDataElements() - 1) == msg.getElement(1)) { 286 log.debug("message lengths match"); 287 waitingForMore = true; 288 try { 289 Thread.sleep(10); 290 } catch (InterruptedException ex) { 291 log.debug("InterruptedException", ex); 292 } 293 //log.debug("We do not forward the response to the listener as it has not been formed"); 294 lastIncomplete = null; 295 forwardToPort(Dcc4PcMessage.getResponse()); 296 297 return; 298 } else { 299 if (log.isDebugEnabled()) { 300 log.debug("Not all of the command was sent, we need to figure out a way to resend the bits"); 301 log.debug("Original Message length {}", mLastSentMessage.getNumDataElements()); 302 log.debug("What CID has procced in size {}", (byte) msg.getElement(1)); 303 log.debug("Reply is in error {}", ((Dcc4PcReply) msg).toHexString()); 304 } 305 } 306 } else if (mLastSentMessage.getElement(0) == 0x0C) { 307 if (log.isDebugEnabled()) { 308 log.debug("last message was a get response {}", ((Dcc4PcReply) msg).toHexString()); 309 } 310 if (msg.getElement(0) == Dcc4PcReply.SUCCESS) { 311 ((Dcc4PcReply) msg).strip(); 312 if (lastIncomplete != null) { 313 //log.debug("Need to add the new reply to this message"); 314 //log.debug("existing : " + lastIncomplete.toHexString()); 315 316 //Append this message to the last incomplete message 317 if (msg.getNumDataElements() != 0) { 318 int iOrig = lastIncomplete.getNumDataElements(); 319 int iNew = 0; 320 while (iNew < msg.getNumDataElements()) { 321 lastIncomplete.setElement(iOrig, msg.getElement(iNew)); 322 iOrig++; 323 iNew++; 324 } 325 } 326 //set the last incomplete message as the one to return 327 log.debug("Reply set as lastIncomplete"); 328 msg = lastIncomplete; 329 } 330 ((Dcc4PcReply) msg).setError(false); 331 ((Dcc4PcReply)msg).setOriginalRequest(mLastMessage); 332 lastIncomplete = null; 333 waitingForMore = false; 334 mLastMessage = null; 335 mLastSentMessage = null; 336 } else if (msg.getElement(0) == Dcc4PcReply.INCOMPLETE) { 337 waitingForMore = true; 338 ((Dcc4PcReply) msg).strip(); 339 if (lastIncomplete != null) { 340 //Append this message to the last incomplete message 341 if (msg.getNumDataElements() != 0) { 342 int iOrig = lastIncomplete.getNumDataElements(); 343 int iNew = 0; 344 while (iNew < msg.getNumDataElements()) { 345 lastIncomplete.setElement(iOrig, msg.getElement(iNew)); 346 iOrig++; 347 iNew++; 348 } 349 } 350 351 } else if (msg.getNumDataElements() > 1) { 352 lastIncomplete = (Dcc4PcReply) msg; 353 } 354 //We do not forward the response to the listener as it has not been formed 355 forwardToPort(Dcc4PcMessage.getResponse()); 356 357 return; 358 359 } else { 360 log.debug("Reply is an error mesage"); 361 ((Dcc4PcReply) msg).setError(true); 362 mLastMessage.setRetries(mLastMessage.getRetries() - 1); 363 if (mLastMessage.getRetries() >= 0) { 364 synchronized (xmtRunnable) { 365 mCurrentState = AUTORETRYSTATE; 366 replyInDispatch = false; 367 xmtRunnable.notify(); 368 } 369 return; 370 } 371 } 372 } 373 } else { 374 log.debug("Last message sent was null {}", ((Dcc4PcReply) msg).toHexString()); 375 } 376 377 // message is complete, dispatch it !! 378 replyInDispatch = true; 379 if (log.isDebugEnabled()) { 380 log.debug("dispatch reply of length {} contains {} state {}", msg.getNumDataElements(), msg.toString(), mCurrentState); 381 } 382 // forward the message to the registered recipients, 383 // which includes the communications monitor 384 // return a notification via the Swing event queue to ensure proper thread 385 Runnable r = newRcvNotifier(msg, mLastSender, this); 386 try { 387 javax.swing.SwingUtilities.invokeAndWait(r); 388 } catch (InterruptedException | InvocationTargetException e) { 389 log.error("Unexpected exception in invokeAndWait:", e); 390 } 391 392 if (log.isDebugEnabled()) { 393 log.debug("dispatch thread invoked"); 394 } 395 if (!msg.isUnsolicited()) { 396 // effect on transmit: 397 switch (mCurrentState) { 398 case WAITMSGREPLYSTATE: { 399 // check to see if the response was an error message we want 400 // to automatically handle by re-queueing the last sent 401 // message, otherwise go on to the next message 402 if (msg.isRetransmittableErrorMsg()) { 403 if (log.isDebugEnabled()) { 404 log.debug("Automatic Recovery from Error Message: {}", msg.toString()); 405 } 406 synchronized (xmtRunnable) { 407 mCurrentState = AUTORETRYSTATE; 408 replyInDispatch = false; 409 xmtRunnable.notify(); 410 } 411 } else { 412 // update state, and notify to continue 413 synchronized (xmtRunnable) { 414 mCurrentState = NOTIFIEDSTATE; 415 replyInDispatch = false; 416 xmtRunnable.notify(); 417 } 418 } 419 break; 420 } 421 case WAITREPLYINPROGMODESTATE: { 422 // entering programming mode 423 mCurrentMode = PROGRAMINGMODE; 424 replyInDispatch = false; 425 426 // check to see if we need to delay to allow decoders to become 427 // responsive 428 int warmUpDelay = enterProgModeDelayTime(); 429 if (warmUpDelay != 0) { 430 try { 431 synchronized (xmtRunnable) { 432 xmtRunnable.wait(warmUpDelay); 433 } 434 } catch (InterruptedException e) { 435 Thread.currentThread().interrupt(); // retain if needed later 436 } 437 } 438 // update state, and notify to continue 439 synchronized (xmtRunnable) { 440 mCurrentState = OKSENDMSGSTATE; 441 xmtRunnable.notify(); 442 } 443 break; 444 } 445 case WAITREPLYINNORMMODESTATE: { 446 // entering normal mode 447 mCurrentMode = NORMALMODE; 448 replyInDispatch = false; 449 // update state, and notify to continue 450 synchronized (xmtRunnable) { 451 mCurrentState = OKSENDMSGSTATE; 452 xmtRunnable.notify(); 453 } 454 break; 455 } 456 default: { 457 replyInDispatch = false; 458 unexpectedReplyStateError(mCurrentState,msg.toString()); 459 } 460 } 461 // Unsolicited message 462 } else { 463 if (log.isDebugEnabled()) { 464 log.debug("Unsolicited Message Received {}", msg.toString()); 465 } 466 replyInDispatch = false; 467 } 468 } 469 470 boolean normalFlushReceiveChars = false; 471 472 //Need a way to detect that the dsr has gone low. 473 @Override 474 protected void loadChars(AbstractMRReply msg, DataInputStream istream) 475 throws java.io.IOException { 476 int i; 477 readingData = false; 478 MAINGET: 479 { 480 for (i = 0; i < msg.maxSize(); i++) { 481 boolean waiting = true; 482 while (waiting) { 483 if (controller.getInputStream().available() > 0) { 484 readingData = true; 485 byte char1 = readByteProtected(istream); 486 waiting = false; 487 488 //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 489 // if there was a timeout, flush any char received and start over 490 if (flushReceiveChars) { 491 lastIncomplete = null; 492 waitingForMore = false; 493 mLastMessage = null; 494 mLastSentMessage = null; 495 readingData = false; 496 log.warn("timeout flushes receive buffer: {}", ((Dcc4PcReply) msg).toHexString()); 497 msg.flush(); 498 i = 0; // restart 499 flushReceiveChars = false; 500 waiting = true; 501 } else { 502 if (canReceive()) { 503 if (log.isDebugEnabled()) { 504 log.debug("Set data {}, {}", i, char1 & 0xff); 505 } 506 msg.setElement(i, char1); 507 waiting = false; 508 if (port.isRI()) { 509 log.debug("Ring high error"); 510 ((Dcc4PcReply) msg).setError(true); 511 break MAINGET; 512 } 513 if (endOfMessage(msg)) { 514 break MAINGET; 515 } 516 } else { 517 i--; // flush char 518 log.error("unsolicited character received: {}", Integer.toHexString(char1)); 519 } 520 } 521 } else if (!port.isDSR()) { 522 if (i == 0) { 523 waiting = true; 524 } else { 525 log.debug("We have data so will break"); 526 waiting = false; 527 break MAINGET; 528 } 529 } else { 530 //As we have no data to process we will set the readingData flag false; 531 readingData = false; 532 } 533 } 534 } 535 } 536 } 537 538 boolean readingData = false; 539 540 @Override 541 protected void transmitWait(int waitTime, int state, String InterruptMessage) { 542 // wait() can have spurious wakeup! 543 // so we protect by making sure the entire timeout time is used 544 long currentTime = Calendar.getInstance().getTimeInMillis(); 545 long endTime = currentTime + waitTime; 546 while (endTime > (currentTime = Calendar.getInstance().getTimeInMillis())) { 547 long wait = endTime - currentTime; 548 try { 549 synchronized (xmtRunnable) { 550 // Do not wait if the current state has changed since we 551 // last set it. 552 553 if (mCurrentState != state) { 554 return; 555 } 556 xmtRunnable.wait(wait); // rcvr normally ends this w state change 557 //If we are in the process of reading the data then do not time out. 558 if (readingData) { 559 endTime = endTime + 10; 560 } 561 //if we have received a packet and a seperate message has been sent to retrieve 562 //the reply we will add more time to our wait process. 563 if (waitingForMore) { 564 waitingForMore = false; 565 //if we are in the process of retrieving data, then we shall increase the endTime by 200ms. 566 endTime = endTime + 200; 567 } 568 569 } 570 } catch (InterruptedException e) { 571 Thread.currentThread().interrupt(); // retain if needed later 572 log.error("{} from {}", InterruptMessage, e.getMessage()); 573 } 574 } 575 log.debug("TIMEOUT in transmitWait, mCurrentState:{} {} port dsr {} wait time {}", mCurrentState, state, port.isDSR(), waitTime); 576 } 577 578 private final static Logger log = LoggerFactory.getLogger(Dcc4PcTrafficController.class); 579}