001package jmri.jmrix.dccpp.simulator; 002 003import java.io.DataInputStream; 004import java.io.DataOutputStream; 005import java.io.IOException; 006import java.io.PipedInputStream; 007import java.io.PipedOutputStream; 008import java.time.LocalDateTime; 009import java.time.format.DateTimeFormatter; 010import java.util.LinkedHashMap; 011import java.util.concurrent.ThreadLocalRandom; 012import java.util.regex.Matcher; 013import java.util.regex.Pattern; 014import java.util.regex.PatternSyntaxException; 015import jmri.jmrix.ConnectionStatus; 016import jmri.jmrix.dccpp.DCCppCommandStation; 017import jmri.jmrix.dccpp.DCCppConstants; 018import jmri.jmrix.dccpp.DCCppInitializationManager; 019import jmri.jmrix.dccpp.DCCppMessage; 020import jmri.jmrix.dccpp.DCCppPacketizer; 021import jmri.jmrix.dccpp.DCCppReply; 022import jmri.jmrix.dccpp.DCCppSimulatorPortController; 023import jmri.jmrix.dccpp.DCCppTrafficController; 024import jmri.util.ImmediatePipedOutputStream; 025import org.slf4j.Logger; 026import org.slf4j.LoggerFactory; 027 028import edu.umd.cs.findbugs.annotations.*; 029 030/** 031 * Provide access to a simulated DCC++ system. 032 * 033 * Currently, the DCCppSimulator reacts to commands sent from the user interface 034 * with messages an appropriate reply message. 035 * 036 * NOTE: Most DCC++ commands are still unsupported in this implementation. 037 * 038 * Normally controlled by the dccpp.DCCppSimulator.DCCppSimulatorFrame class. 039 * 040 * NOTE: Some material in this file was modified from other portions of the 041 * support infrastructure. 042 * 043 * @author Paul Bender, Copyright (C) 2009-2010 044 * @author Mark Underwood, Copyright (C) 2015 045 * @author M Steve Todd, 2021 046 * 047 * Based on {@link jmri.jmrix.lenz.xnetsimulator.XNetSimulatorAdapter} 048 */ 049public class DCCppSimulatorAdapter extends DCCppSimulatorPortController implements Runnable { 050 051 final static int SENSOR_MSG_RATE = 10; 052 053 private boolean outputBufferEmpty = true; 054 private final boolean checkBuffer = true; 055 private boolean trackPowerState = false; 056 // One extra array element so that i can index directly from the 057 // CV value, ignoring CVs[0]. 058 private final int[] CVs = new int[DCCppConstants.MAX_DIRECT_CV + 1]; 059 060 private java.util.TimerTask keepAliveTimer; // Timer used to periodically 061 private static final long keepAliveTimeoutValue = 30000; // Interval 062 //keep track of recreation command, including state, for each turnout and output 063 private LinkedHashMap<Integer,String> turnouts = new LinkedHashMap<Integer, String>(); 064 //keep track of speed, direction and functions for each loco address 065 private LinkedHashMap<Integer,Integer> locoSpeedByte = new LinkedHashMap<Integer,Integer>(); 066 private LinkedHashMap<Integer,Integer> locoFunctions = new LinkedHashMap<Integer,Integer>(); 067 068 public DCCppSimulatorAdapter() { 069 setPort(Bundle.getMessage("None")); 070 try { 071 PipedOutputStream tempPipeI = new ImmediatePipedOutputStream(); 072 pout = new DataOutputStream(tempPipeI); 073 inpipe = new DataInputStream(new PipedInputStream(tempPipeI)); 074 PipedOutputStream tempPipeO = new ImmediatePipedOutputStream(); 075 outpipe = new DataOutputStream(tempPipeO); 076 pin = new DataInputStream(new PipedInputStream(tempPipeO)); 077 } catch (java.io.IOException e) { 078 log.error("init (pipe): Exception: {}", e.toString()); 079 return; 080 } 081 // Zero out the CV table. 082 for (int i = 0; i < DCCppConstants.MAX_DIRECT_CV + 1; i++) { 083 CVs[i] = 0; 084 } 085 } 086 087 @Override 088 public String openPort(String portName, String appName) { 089 // open the port in XpressNet mode, check ability to set moderators 090 setPort(portName); 091 return null; // normal operation 092 } 093 094 /** 095 * Set if the output buffer is empty or full. This should only be set to 096 * false by external processes. 097 * 098 * @param s true if output buffer is empty; false otherwise 099 */ 100 @Override 101 synchronized public void setOutputBufferEmpty(boolean s) { 102 outputBufferEmpty = s; 103 } 104 105 /** 106 * Can the port accept additional characters? The state of CTS determines 107 * this, as there seems to be no way to check the number of queued bytes and 108 * buffer length. This might go false for short intervals, but it might also 109 * stick off if something goes wrong. 110 * 111 * @return true if port can accept additional characters; false otherwise 112 */ 113 @Override 114 public boolean okToSend() { 115 if (checkBuffer) { 116 log.debug("Buffer Empty: {}", outputBufferEmpty); 117 return (outputBufferEmpty); 118 } else { 119 log.debug("No Flow Control or Buffer Check"); 120 return (true); 121 } 122 } 123 124 /** 125 * Set up all of the other objects to operate with a DCCppSimulator 126 * connected to this port 127 */ 128 @Override 129 public void configure() { 130 // connect to a packetizing traffic controller 131 DCCppTrafficController packets = new DCCppPacketizer(new DCCppCommandStation()); 132 packets.connectPort(this); 133 134 // start operation 135 // packets.startThreads(); 136 this.getSystemConnectionMemo().setDCCppTrafficController(packets); 137 138 sourceThread = jmri.util.ThreadingUtil.newThread(this); 139 sourceThread.start(); 140 141 new DCCppInitializationManager(this.getSystemConnectionMemo()); 142 } 143 144 /** 145 * Set up the keepAliveTimer, and start it. 146 */ 147 private void keepAliveTimer() { 148 if (keepAliveTimer == null) { 149 keepAliveTimer = new java.util.TimerTask(){ 150 @Override 151 public void run() { 152 // If the timer times out, send a request for status 153 DCCppSimulatorAdapter.this.getSystemConnectionMemo().getDCCppTrafficController() 154 .sendDCCppMessage(jmri.jmrix.dccpp.DCCppMessage.makeCSStatusMsg(), null); 155 } 156 }; 157 } else { 158 keepAliveTimer.cancel(); 159 } 160 jmri.util.TimerUtil.schedule(keepAliveTimer, keepAliveTimeoutValue, keepAliveTimeoutValue); 161 } 162 163 164 // base class methods for the DCCppSimulatorPortController interface 165 166 /** 167 * {@inheritDoc} 168 */ 169 @Override 170 public DataInputStream getInputStream() { 171 if (pin == null) { 172 log.error("getInputStream called before load(), stream not available"); 173 ConnectionStatus.instance().setConnectionState(getUserName(), getCurrentPortName(), ConnectionStatus.CONNECTION_DOWN); 174 } 175 return pin; 176 } 177 178 /** 179 * {@inheritDoc} 180 */ 181 @Override 182 public DataOutputStream getOutputStream() { 183 if (pout == null) { 184 log.error("getOutputStream called before load(), stream not available"); 185 ConnectionStatus.instance().setConnectionState(getUserName(), getCurrentPortName(), ConnectionStatus.CONNECTION_DOWN); 186 } 187 return pout; 188 } 189 190 /** 191 * {@inheritDoc} 192 */ 193 @Override 194 public boolean status() { 195 return (pout != null && pin != null); 196 } 197 198 /** 199 * {@inheritDoc} 200 * Currently just a message saying it's fixed. 201 * 202 * @return null 203 */ 204 @Override 205 public String[] validBaudRates() { 206 return new String[]{}; 207 } 208 209 /** 210 * {@inheritDoc} 211 */ 212 @Override 213 public int[] validBaudNumbers() { 214 return new int[]{}; 215 } 216 217 @Override 218 public void run() { // start a new thread 219 // this thread has one task. It repeatedly reads from the input pipe 220 // and writes modified data to the output pipe. This is the heart 221 // of the command station simulation. 222 log.debug("Simulator Thread Started"); 223 224 keepAliveTimer(); 225 226 ConnectionStatus.instance().setConnectionState(getUserName(), getCurrentPortName(), ConnectionStatus.CONNECTION_UP); 227 for (;;) { 228 DCCppMessage m = readMessage(); 229 log.debug("Simulator Thread received message '{}'", m); 230 DCCppReply r = generateReply(m); 231 // If generateReply() returns null, do nothing. No reply to send. 232 if (r != null) { 233 writeReply(r); 234 } 235 236 // Once every SENSOR_MSG_RATE loops, generate a random Sensor message. 237 int rand = ThreadLocalRandom.current().nextInt(SENSOR_MSG_RATE); 238 if (rand == 1) { 239 generateRandomSensorReply(); 240 } 241 } 242 } 243 244 // readMessage reads one incoming message from the buffer 245 // and sets outputBufferEmpty to true. 246 private DCCppMessage readMessage() { 247 DCCppMessage msg = null; 248 try { 249 msg = loadChars(); 250 } catch (java.io.IOException e) { 251 // should do something meaningful here. 252 ConnectionStatus.instance().setConnectionState(getUserName(), getCurrentPortName(), ConnectionStatus.CONNECTION_DOWN); 253 254 } 255 setOutputBufferEmpty(true); 256 return (msg); 257 } 258 259 // generateReply is the heart of the simulation. It translates an 260 // incoming DCCppMessage into an outgoing DCCppReply. 261 @SuppressFBWarnings( value="FS_BAD_DATE_FORMAT_FLAG_COMBO", justification = "both am/pm and 24hr flags present ok as only used for display output") 262 private DCCppReply generateReply(DCCppMessage msg) { 263 String s, r = null; 264 Pattern p; 265 Matcher m; 266 DCCppReply reply = null; 267 268 log.debug("Generate Reply to message type '{}' string = '{}'", msg.getElement(0), msg); 269 270 switch (msg.getElement(0)) { 271 272 case DCCppConstants.THROTTLE_CMD: 273 log.debug("THROTTLE_CMD detected"); 274 s = msg.toString(); 275 try { 276 p = Pattern.compile(DCCppConstants.THROTTLE_CMD_REGEX); 277 m = p.matcher(s); //<t REG CAB SPEED DIR> 278 if (!m.matches()) { 279 p = Pattern.compile(DCCppConstants.THROTTLE_V3_CMD_REGEX); 280 m = p.matcher(s); //<t locoId speed dir> 281 if (!m.matches()) { 282 log.error("Malformed Throttle Command: {}", s); 283 return (null); 284 } 285 int locoId = Integer.parseInt(m.group(1)); 286 int speed = Integer.parseInt(m.group(2)); 287 int dir = Integer.parseInt(m.group(3)); 288 storeLocoSpeedByte(locoId, speed, dir); 289 r = getLocoStateString(locoId); 290 } else { 291 r = "T " + m.group(1) + " " + m.group(3) + " " + m.group(4); 292 } 293 } catch (PatternSyntaxException e) { 294 log.error("Malformed pattern syntax! "); 295 return (null); 296 } catch (IllegalStateException e) { 297 log.error("Group called before match operation executed string= {}", s); 298 return (null); 299 } catch (IndexOutOfBoundsException e) { 300 log.error("Index out of bounds string= {}", s); 301 return (null); 302 } 303 reply = DCCppReply.parseDCCppReply(r); 304 log.debug("Reply generated = '{}'", reply); 305 break; 306 307 case DCCppConstants.FUNCTION_V4_CMD: 308 log.debug("FunctionV4Detected"); 309 s = msg.toString(); 310 r = ""; 311 try { 312 p = Pattern.compile(DCCppConstants.FUNCTION_V4_CMD_REGEX); 313 m = p.matcher(s); //<F locoId func 1|0> 314 if (!m.matches()) { 315 log.error("Malformed FunctionV4 Command: {}", s); 316 return (null); 317 } 318 int locoId = Integer.parseInt(m.group(1)); 319 int fn = Integer.parseInt(m.group(2)); 320 int state = Integer.parseInt(m.group(3)); 321 storeLocoFunction(locoId, fn, state); 322 r = getLocoStateString(locoId); 323 } catch (PatternSyntaxException e) { 324 log.error("Malformed pattern syntax!"); 325 return (null); 326 } catch (IllegalStateException e) { 327 log.error("Group called before match operation executed string= {}", s); 328 return (null); 329 } catch (IndexOutOfBoundsException e) { 330 log.error("Index out of bounds string= {}", s); 331 return (null); 332 } 333 reply = DCCppReply.parseDCCppReply(r); 334 log.debug("Reply generated = '{}'", reply); 335 break; 336 337 case DCCppConstants.TURNOUT_CMD: 338 if (msg.isTurnoutAddMessage() 339 || msg.isTurnoutAddDCCMessage() 340 || msg.isTurnoutAddServoMessage() 341 || msg.isTurnoutAddVpinMessage()) { 342 log.debug("Add Turnout Message"); 343 s = "H" + msg.toString().substring(1) + " 0"; //T reply is H, init to closed 344 turnouts.put(msg.getTOIDInt(), s); 345 r = "O"; 346 } else if (msg.isTurnoutDeleteMessage()) { 347 log.debug("Delete Turnout Message"); 348 turnouts.remove(msg.getTOIDInt()); 349 r = "O"; 350 } else if (msg.isListTurnoutsMessage()) { 351 log.debug("List Turnouts Message"); 352 generateTurnoutListReply(); 353 break; 354 } else if (msg.isTurnoutCmdMessage()) { 355 log.debug("Turnout Command Message"); 356 s = turnouts.get(msg.getTOIDInt()); //retrieve the stored turnout def 357 if (s != null) { 358 s = s.substring(0, s.length()-1) + msg.getTOStateInt(); //replace the last char with new state 359 turnouts.put(msg.getTOIDInt(), s); //update the stored turnout 360 r = "H " + msg.getTOIDString() + " " + msg.getTOStateInt(); 361 } else { 362 log.warn("Unknown turnout ID '{}'", msg.getTOIDInt()); 363 r = "X"; 364 } 365 366 } else { 367 log.debug("Unknown TURNOUT_CMD detected"); 368 r = "X"; 369 } 370 reply = DCCppReply.parseDCCppReply(r); 371 log.debug("Reply generated = '{}'", reply); 372 break; 373 374 case DCCppConstants.OUTPUT_CMD: 375 if (msg.isOutputCmdMessage()) { 376 log.debug("Output Command Message: '{}'", msg); 377 s = turnouts.get(msg.getOutputIDInt()); //retrieve the stored turnout def 378 if (s != null) { 379 s = s.substring(0, s.length()-1) + (msg.getOutputStateBool() ? "1" : "0"); //replace the last char with new state 380 turnouts.put(msg.getOutputIDInt(), s); //update the stored turnout 381 r = "Y " + msg.getOutputIDInt() + " " + (msg.getOutputStateBool() ? "1" : "0"); 382 reply = DCCppReply.parseDCCppReply(r); 383 log.debug("Reply generated = {}", reply.toString()); 384 } else { 385 log.warn("Unknown output ID '{}'", msg.getOutputIDInt()); 386 r = "X"; 387 } 388 } else if (msg.isOutputAddMessage()) { 389 log.debug("Output Add Message"); 390 s = "Y" + msg.toString().substring(1) + " 0"; //Z reply is Y, init to closed 391 turnouts.put(msg.getOutputIDInt(), s); 392 r = "O"; 393 } else if (msg.isOutputDeleteMessage()) { 394 log.debug("Output Delete Message"); 395 turnouts.remove(msg.getOutputIDInt()); 396 r = "O"; 397 } else if (msg.isListOutputsMessage()) { 398 log.debug("Output List Message"); 399 generateTurnoutListReply(); 400 break; 401 } else { 402 log.error("Unknown Output Command: '{}'", msg.toString()); 403 r = "X"; 404 } 405 reply = DCCppReply.parseDCCppReply(r); 406 log.debug("Reply generated = '{}'", reply); 407 break; 408 409 case DCCppConstants.SENSOR_CMD: 410 if (msg.isSensorAddMessage()) { 411 log.debug("SENSOR_CMD Add detected"); 412 //s = msg.toString(); 413 r = "O"; // TODO: Randomize? 414 } else if (msg.isSensorDeleteMessage()) { 415 log.debug("SENSOR_CMD Delete detected"); 416 //s = msg.toString(); 417 r = "O"; // TODO: Randomize? 418 } else if (msg.isListSensorsMessage()) { 419 r = "Q 1 4 1"; // TODO: DO this for real. 420 } else { 421 log.debug("Invalid SENSOR_CMD detected"); 422 r = "X"; 423 } 424 reply = DCCppReply.parseDCCppReply(r); 425 log.debug("Reply generated = '{}'", reply); 426 break; 427 428 case DCCppConstants.PROG_WRITE_CV_BYTE: 429 log.debug("PROG_WRITE_CV_BYTE detected"); 430 s = msg.toString(); 431 r = ""; 432 try { 433 if (s.matches(DCCppConstants.PROG_WRITE_BYTE_REGEX)) { 434 p = Pattern.compile(DCCppConstants.PROG_WRITE_BYTE_REGEX); 435 m = p.matcher(s); 436 if (!m.matches()) { 437 log.error("Malformed ProgWriteCVByte Command: {}", s); 438 return (null); 439 } 440 // CMD: <W CV Value CALLBACKNUM CALLBACKSUB> 441 // Response: <r CALLBACKNUM|CALLBACKSUB|CV Value> 442 r = "r " + m.group(3) + "|" + m.group(4) + "|" + m.group(1) + 443 " " + m.group(2); 444 CVs[Integer.parseInt(m.group(1))] = Integer.parseInt(m.group(2)); 445 } else if (s.matches(DCCppConstants.PROG_WRITE_BYTE_V4_REGEX)) { 446 p = Pattern.compile(DCCppConstants.PROG_WRITE_BYTE_V4_REGEX); 447 m = p.matcher(s); 448 if (!m.matches()) { 449 log.error("Malformed ProgWriteCVByte Command: {}", s); 450 return (null); 451 } 452 // CMD: <W CV Value> 453 // Response: <r CV Value> 454 r = "r " + m.group(1) + " " + m.group(2); 455 CVs[Integer.parseInt(m.group(1))] = Integer.parseInt(m.group(2)); 456 } 457 reply = DCCppReply.parseDCCppReply(r); 458 log.debug("Reply generated = {}", reply.toString()); 459 } catch (PatternSyntaxException e) { 460 log.error("Malformed pattern syntax!"); 461 return (null); 462 } catch (IllegalStateException e) { 463 log.error("Group called before match operation executed string= {}", s); 464 return (null); 465 } catch (IndexOutOfBoundsException e) { 466 log.error("Index out of bounds string= {}", s); 467 return (null); 468 } 469 break; 470 471 case DCCppConstants.PROG_WRITE_CV_BIT: 472 log.debug("PROG_WRITE_CV_BIT detected"); 473 s = msg.toString(); 474 try { 475 p = Pattern.compile(DCCppConstants.PROG_WRITE_BIT_REGEX); 476 m = p.matcher(s); 477 if (!m.matches()) { 478 log.error("Malformed ProgWriteCVBit Command: {}", s); 479 return (null); 480 } 481 // CMD: <B CV BIT Value CALLBACKNUM CALLBACKSUB> 482 // Response: <r CALLBACKNUM|CALLBACKSUB|CV BIT Value> 483 r = "r " + m.group(4) + "|" + m.group(5) + "|" + m.group(1) + " " 484 + m.group(2) + m.group(3); 485 int idx = Integer.parseInt(m.group(1)); 486 int bit = Integer.parseInt(m.group(2)); 487 int v = Integer.parseInt(m.group(3)); 488 if (v == 1) { 489 CVs[idx] = CVs[idx] | (0x0001 << bit); 490 } else { 491 CVs[idx] = CVs[idx] & ~(0x0001 << bit); 492 } 493 reply = DCCppReply.parseDCCppReply(r); 494 log.debug("Reply generated = {}", reply.toString()); 495 } catch (PatternSyntaxException e) { 496 log.error("Malformed pattern syntax!"); 497 return (null); 498 } catch (IllegalStateException e) { 499 log.error("Group called before match operation executed string= {}", s); 500 return (null); 501 } catch (IndexOutOfBoundsException e) { 502 log.error("Index out of bounds string= {}", s); 503 return (null); 504 } 505 break; 506 507 case DCCppConstants.PROG_READ_CV: 508 log.debug("PROG_READ_CV detected"); 509 s = msg.toString(); 510 r = ""; 511 try { 512 if (s.matches(DCCppConstants.PROG_READ_CV_REGEX)) { 513 p = Pattern.compile(DCCppConstants.PROG_READ_CV_REGEX); 514 m = p.matcher(s); 515 int cv = Integer.parseInt(m.group(1)); 516 int cvVal = 0; // Default to 0 if they're reading out of bounds. 517 if (cv < CVs.length) { 518 cvVal = CVs[Integer.parseInt(m.group(1))]; 519 } 520 // CMD: <R CV CALLBACKNUM CALLBACKSUB> 521 // Response: <r CALLBACKNUM|CALLBACKSUB|CV Value> 522 r = "r " + m.group(2) + "|" + m.group(3) + "|" + m.group(1) + " " 523 + cvVal; 524 } else if (s.matches(DCCppConstants.PROG_READ_CV_V4_REGEX)) { 525 p = Pattern.compile(DCCppConstants.PROG_READ_CV_V4_REGEX); 526 m = p.matcher(s); 527 if (!m.matches()) { 528 log.error("Malformed PROG_READ_CV Command: {}", s); 529 return (null); 530 } 531 int cv = Integer.parseInt(m.group(1)); 532 int cvVal = 0; // Default to 0 if they're reading out of bounds. 533 if (cv < CVs.length) { 534 cvVal = CVs[Integer.parseInt(m.group(1))]; 535 } 536 // CMD: <R CV> 537 // Response: <r CV Value> 538 r = "r " + m.group(1) + " " + cvVal; 539 } else if (s.matches(DCCppConstants.PROG_READ_LOCOID_REGEX)) { 540 int locoId = ThreadLocalRandom.current().nextInt(9999)+1; //get a random locoId between 1 and 9999 541 // CMD: <R> 542 // Response: <r LocoId> 543 r = "r " + locoId; 544 } else { 545 log.error("Malformed PROG_READ_CV Command: {}", s); 546 return (null); 547 } 548 549 reply = DCCppReply.parseDCCppReply(r); 550 log.debug("Reply generated = {}", reply.toString()); 551 } catch (PatternSyntaxException e) { 552 log.error("Malformed pattern syntax!"); 553 return (null); 554 } catch (IllegalStateException e) { 555 log.error("Group called before match operation executed string= {}", s); 556 return (null); 557 } catch (IndexOutOfBoundsException e) { 558 log.error("Index out of bounds string= {}", s); 559 return (null); 560 } 561 break; 562 563 case DCCppConstants.PROG_VERIFY_CV: 564 log.debug("PROG_VERIFY_CV detected"); 565 s = msg.toString(); 566 try { 567 p = Pattern.compile(DCCppConstants.PROG_VERIFY_REGEX); 568 m = p.matcher(s); 569 if (!m.matches()) { 570 log.error("Malformed PROG_VERIFY_CV Command: {}", s); 571 return (null); 572 } 573 // TODO: Work Magic Here to retrieve stored value. 574 // Make sure that CV exists 575 int cv = Integer.parseInt(m.group(1)); 576 int cvVal = 0; // Default to 0 if they're reading out of bounds. 577 if (cv < CVs.length) { 578 cvVal = CVs[cv]; 579 } 580 // CMD: <V CV STARTVAL> 581 // Response: <v CV Value> 582 r = "v " + cv + " " + cvVal; 583 584 reply = DCCppReply.parseDCCppReply(r); 585 log.debug("Reply generated = {}", reply.toString()); 586 } catch (PatternSyntaxException e) { 587 log.error("Malformed pattern syntax!"); 588 return (null); 589 } catch (IllegalStateException e) { 590 log.error("Group called before match operation executed string= {}", s); 591 return (null); 592 } catch (IndexOutOfBoundsException e) { 593 log.error("Index out of bounds string= {}", s); 594 return (null); 595 } 596 break; 597 598 case DCCppConstants.TRACK_POWER_ON: 599 log.debug("TRACK_POWER_ON detected"); 600 trackPowerState = true; 601 reply = DCCppReply.parseDCCppReply("p1"); 602 break; 603 604 case DCCppConstants.TRACK_POWER_OFF: 605 log.debug("TRACK_POWER_OFF detected"); 606 trackPowerState = false; 607 reply = DCCppReply.parseDCCppReply("p0"); 608 break; 609 610 case DCCppConstants.READ_MAXNUMSLOTS: 611 log.debug("READ_MAXNUMSLOTS detected"); 612 reply = DCCppReply.parseDCCppReply("# 12"); 613 break; 614 615 case DCCppConstants.READ_TRACK_CURRENT: 616 log.debug("READ_TRACK_CURRENT detected"); 617 generateMeterReplies(); 618 break; 619 620 case DCCppConstants.TRACKMANAGER_CMD: 621 log.debug("TRACKMANAGER_CMD detected"); 622 reply = DCCppReply.parseDCCppReply("= A MAIN"); 623 writeReply(reply); 624 reply = DCCppReply.parseDCCppReply("= B PROG"); 625 break; 626 627 case DCCppConstants.LCD_TEXT_CMD: 628 log.debug("LCD_TEXT_CMD detected"); 629 DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss a"); 630 LocalDateTime now = LocalDateTime.now(); 631 String dateTimeString = now.format(formatter); 632 reply = DCCppReply.parseDCCppReply("@ 0 0 \"Welcome to DCC-EX -- " + dateTimeString + "\"" ); 633 writeReply(reply); 634 reply = DCCppReply.parseDCCppReply("@ 0 1 \"LCD Line 1\""); 635 writeReply(reply); 636 reply = DCCppReply.parseDCCppReply("@ 0 2 \"LCD Line 2\""); 637 writeReply(reply); 638 reply = DCCppReply.parseDCCppReply("@ 0 3 \" LCD Line 3 with spaces \""); 639 writeReply(reply); 640 reply = DCCppReply.parseDCCppReply("@ 0 4 \"1234567890123456789012345678901234567890\""); 641 break; 642 643 case DCCppConstants.READ_CS_STATUS: 644 log.debug("READ_CS_STATUS detected"); 645 generateReadCSStatusReply(); // Handle this special. 646 break; 647 648 case DCCppConstants.FUNCTION_CMD: 649 case DCCppConstants.FORGET_CAB_CMD: 650 case DCCppConstants.ACCESSORY_CMD: 651 case DCCppConstants.OPS_WRITE_CV_BYTE: 652 case DCCppConstants.OPS_WRITE_CV_BIT: 653 case DCCppConstants.WRITE_DCC_PACKET_MAIN: 654 case DCCppConstants.WRITE_DCC_PACKET_PROG: 655 log.debug("non-reply message detected: '{}'", msg); 656 // Send no reply. 657 return (null); 658 659 default: 660 log.debug("unknown message detected: '{}'", msg); 661 return (null); 662 } 663 return (reply); 664 } 665 666 //calc speedByte value matching DCC++EX, then store it, so it can be used in the locoState replies 667 private void storeLocoSpeedByte(int locoId, int speed, int dir) { 668 if (speed>0) speed++; //add 1 to speed if not zero or estop 669 if (speed<0) speed = 1; //eStop is actually 1 670 int dirBit = dir*128; //calc value for direction bit 671 int speedByte = dirBit + speed; //add dirBit to adjusted speed value 672 locoSpeedByte.put(locoId, speedByte); //store it 673 if (!locoFunctions.containsKey(locoId)) locoFunctions.put(locoId, 0); //init functions if not set 674 } 675 676 //stores the calculated value of the functionsByte as used by DCC++EX 677 private void storeLocoFunction(int locoId, int function, int state) { 678 int functions = 0; //init functions to all off if not stored 679 if (locoFunctions.containsKey(locoId)) 680 functions = locoFunctions.get(locoId); //get stored value, if any 681 int mask = 1 << function; 682 if (state == 1) { 683 functions = functions | mask; //apply ON 684 } else { 685 functions = functions & ~mask; //apply OFF 686 } 687 locoFunctions.put(locoId, functions); //store new value 688 if (!locoSpeedByte.containsKey(locoId)) 689 locoSpeedByte.put(locoId, 0); //init speedByte if not set 690 } 691 692 //retrieve stored values and calculate and format the locostate message text 693 private String getLocoStateString(int locoId) { 694 String s; 695 int speedByte = locoSpeedByte.get(locoId); 696 int functions = locoFunctions.get(locoId); 697 s = "l " + locoId + " 0 " + speedByte + " " + functions; //<l loco slot speedByte functions> 698 return s; 699 } 700 701 /* 's'tatus message gets multiple reply messages */ 702 private void generateReadCSStatusReply() { 703 DCCppReply r = new DCCppReply("p" + (trackPowerState ? "1" : "0")); 704 writeReply(r); 705 r = DCCppReply.parseDCCppReply("iDCC-EX V-4.0.1 / MEGA / STANDARD_MOTOR_SHIELD G-9db6d36"); 706 writeReply(r); 707 generateTurnoutStatesReply(); 708 } 709 710 /* Send list of creation command with states for all defined turnouts and outputs */ 711 private void generateTurnoutListReply() { 712 if (!turnouts.isEmpty()) { 713 turnouts.forEach((key, value) -> { //send back the full create string for each 714 DCCppReply r = new DCCppReply(value); 715 writeReply(r); 716 }); 717 } else { 718 writeReply(new DCCppReply("X No Turnouts Defined")); 719 } 720 } 721 722 /* Send list of turnout states */ 723 private void generateTurnoutStatesReply() { 724 if (!turnouts.isEmpty()) { 725 turnouts.forEach((key, value) -> { 726 String s = value.substring(0,2) + key + value.substring(value.length()-2); //command char + id + state 727 DCCppReply r = new DCCppReply(s); 728 writeReply(r); 729 }); 730 } else { 731 writeReply(new DCCppReply("X No Turnouts Defined")); 732 } 733 } 734 735 /* 'c' current request message gets multiple reply messages */ 736 private void generateMeterReplies() { 737 int currentmA = 1100 + ThreadLocalRandom.current().nextInt(64); 738 double voltageV = 14.5 + ThreadLocalRandom.current().nextInt(10)/10.0; 739 String rs = "c CurrentMAIN " + (trackPowerState ? Double.toString(currentmA) : "0") + " C Milli 0 1997 1 1997"; 740 DCCppReply r = new DCCppReply(rs); 741 writeReply(r); 742 r = new DCCppReply("c VoltageMAIN " + voltageV + " V NoPrefix 0 18.0 0.1 16.0"); 743 writeReply(r); 744 rs = "a " + (trackPowerState ? Integer.toString((1997/currentmA)*100) : "0"); 745 r = DCCppReply.parseDCCppReply(rs); 746 writeReply(r); 747 } 748 749 private void generateRandomSensorReply() { 750 // Pick a random sensor number between 0 and 10; 751 int sensorNum = ThreadLocalRandom.current().nextInt(10)+1; // Generate a random sensor number between 1 and 10 752 int value = ThreadLocalRandom.current().nextInt(2); // Generate state value between 0 and 1 753 754 String reply = (value == 1 ? "Q " : "q ") + sensorNum; 755 756 DCCppReply r = DCCppReply.parseDCCppReply(reply); 757 writeReply(r); 758 } 759 760 private void writeReply(DCCppReply r) { 761 log.debug("Simulator Thread sending Reply '{}'", r); 762 int i; 763 int len = r.getLength(); // opCode+Nbytes+ECC 764 // If r == null, there is no reply to be sent. 765 try { 766 outpipe.writeByte((byte) '<'); 767 for (i = 0; i < len; i++) { 768 outpipe.writeByte((byte) r.getElement(i)); 769 } 770 outpipe.writeByte((byte) '>'); 771 } catch (java.io.IOException ex) { 772 ConnectionStatus.instance().setConnectionState(getUserName(), getCurrentPortName(), ConnectionStatus.CONNECTION_DOWN); 773 } 774 } 775 776 /** 777 * Get characters from the input source, and file a message. 778 * <p> 779 * Returns only when the message is complete. 780 * <p> 781 * Only used in the Receive thread. 782 * 783 * @return filled message 784 * @throws IOException when presented by the input source. 785 */ 786 private DCCppMessage loadChars() throws java.io.IOException { 787 // Spin waiting for start-of-frame '<' character (and toss it) 788 StringBuilder s = new StringBuilder(); 789 byte char1; 790 boolean found_start = false; 791 792 // this loop reads every other character; is that the desired behavior? 793 while (!found_start) { 794 char1 = readByteProtected(inpipe); 795 if ((char1 & 0xFF) == '<') { 796 found_start = true; 797 log.trace("Found starting < "); 798 break; // A bit redundant with setting the loop condition true (false) 799 } else { 800 // drop next character before repeating 801 readByteProtected(inpipe); 802 } 803 } 804 // Now, suck in the rest of the message... 805 for (int i = 0; i < DCCppConstants.MAX_MESSAGE_SIZE; i++) { 806 char1 = readByteProtected(inpipe); 807 if (char1 == '>') { 808 log.trace("msg found > "); 809 // Don't store the > 810 break; 811 } else { 812 log.trace("msg read byte {}", char1); 813 char c = (char) (char1 & 0x00FF); 814 s.append(c); 815 } 816 } 817 // TODO: Still need to strip leading and trailing whitespace. 818 log.debug("Complete message = {}", s); 819 return (new DCCppMessage(s.toString())); 820 } 821 822 /** 823 * Read a single byte, protecting against various timeouts, etc. 824 * <p> 825 * When a port is set to have a receive timeout (via the 826 * enableReceiveTimeout() method), some will return zero bytes or an 827 * EOFException at the end of the timeout. In that case, the read should be 828 * repeated to get the next real character. 829 * @param istream source of data 830 * @return next available byte, when available 831 * @throws IOException from underlying operation 832 * 833 */ 834 protected byte readByteProtected(DataInputStream istream) throws java.io.IOException { 835 byte[] rcvBuffer = new byte[1]; 836 while (true) { // loop will repeat until character found 837 int nchars; 838 nchars = istream.read(rcvBuffer, 0, 1); 839 if (nchars > 0) { 840 return rcvBuffer[0]; 841 } 842 } 843 } 844 845 volatile static DCCppSimulatorAdapter mInstance = null; 846 private DataOutputStream pout = null; // for output to other classes 847 private DataInputStream pin = null; // for input from other classes 848 // internal ends of the pipes 849 private DataOutputStream outpipe = null; // feed pin 850 private DataInputStream inpipe = null; // feed pout 851 private Thread sourceThread; 852 853 private final static Logger log = LoggerFactory.getLogger(DCCppSimulatorAdapter.class); 854 855}