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 // When the timer times out, send a heartbeat (status request on DCC++, max num slots request on DCC-EX 153 DCCppTrafficController tc = DCCppSimulatorAdapter.this.getSystemConnectionMemo().getDCCppTrafficController(); 154 DCCppCommandStation cs = tc.getCommandStation(); 155 if (cs.isMaxNumSlotsMsgSupported()) { 156 tc.sendDCCppMessage(jmri.jmrix.dccpp.DCCppMessage.makeCSMaxNumSlotsMsg(), null); 157 } else { 158 tc.sendDCCppMessage(jmri.jmrix.dccpp.DCCppMessage.makeCSStatusMsg(), null); 159 } 160 161 } 162 }; 163 } else { 164 keepAliveTimer.cancel(); 165 } 166 jmri.util.TimerUtil.schedule(keepAliveTimer, keepAliveTimeoutValue, keepAliveTimeoutValue); 167 } 168 169 170 // base class methods for the DCCppSimulatorPortController interface 171 172 /** 173 * {@inheritDoc} 174 */ 175 @Override 176 public DataInputStream getInputStream() { 177 if (pin == null) { 178 log.error("getInputStream called before load(), stream not available"); 179 ConnectionStatus.instance().setConnectionState(getUserName(), getCurrentPortName(), ConnectionStatus.CONNECTION_DOWN); 180 } 181 return pin; 182 } 183 184 /** 185 * {@inheritDoc} 186 */ 187 @Override 188 public DataOutputStream getOutputStream() { 189 if (pout == null) { 190 log.error("getOutputStream called before load(), stream not available"); 191 ConnectionStatus.instance().setConnectionState(getUserName(), getCurrentPortName(), ConnectionStatus.CONNECTION_DOWN); 192 } 193 return pout; 194 } 195 196 /** 197 * {@inheritDoc} 198 */ 199 @Override 200 public boolean status() { 201 return (pout != null && pin != null); 202 } 203 204 /** 205 * {@inheritDoc} 206 * Currently just a message saying it's fixed. 207 * 208 * @return null 209 */ 210 @Override 211 public String[] validBaudRates() { 212 return new String[]{}; 213 } 214 215 /** 216 * {@inheritDoc} 217 */ 218 @Override 219 public int[] validBaudNumbers() { 220 return new int[]{}; 221 } 222 223 @Override 224 public void run() { // start a new thread 225 // this thread has one task. It repeatedly reads from the input pipe 226 // and writes modified data to the output pipe. This is the heart 227 // of the command station simulation. 228 log.debug("Simulator Thread Started"); 229 230 keepAliveTimer(); 231 232 ConnectionStatus.instance().setConnectionState(getUserName(), getCurrentPortName(), ConnectionStatus.CONNECTION_UP); 233 for (;;) { 234 DCCppMessage m = readMessage(); 235 log.debug("Simulator Thread received message '{}'", m); 236 DCCppReply r = generateReply(m); 237 // If generateReply() returns null, do nothing. No reply to send. 238 if (r != null) { 239 writeReply(r); 240 } 241 242 // Once every SENSOR_MSG_RATE loops, generate a random Sensor message. 243 int rand = ThreadLocalRandom.current().nextInt(SENSOR_MSG_RATE); 244 if (rand == 1) { 245 generateRandomSensorReply(); 246 } 247 } 248 } 249 250 // readMessage reads one incoming message from the buffer 251 // and sets outputBufferEmpty to true. 252 private DCCppMessage readMessage() { 253 DCCppMessage msg = null; 254 try { 255 msg = loadChars(); 256 } catch (java.io.IOException e) { 257 // should do something meaningful here. 258 ConnectionStatus.instance().setConnectionState(getUserName(), getCurrentPortName(), ConnectionStatus.CONNECTION_DOWN); 259 260 } 261 setOutputBufferEmpty(true); 262 return (msg); 263 } 264 265 // generateReply is the heart of the simulation. It translates an 266 // incoming DCCppMessage into an outgoing DCCppReply. 267 @SuppressFBWarnings( value="FS_BAD_DATE_FORMAT_FLAG_COMBO", justification = "both am/pm and 24hr flags present ok as only used for display output") 268 private DCCppReply generateReply(DCCppMessage msg) { 269 String s, r = null; 270 Pattern p; 271 Matcher m; 272 DCCppReply reply = null; 273 274 log.debug("Generate Reply to message type '{}' string = '{}'", msg.getElement(0), msg); 275 276 switch (msg.getElement(0)) { 277 278 case DCCppConstants.THROTTLE_CMD: 279 log.debug("THROTTLE_CMD detected"); 280 s = msg.toString(); 281 try { 282 p = Pattern.compile(DCCppConstants.THROTTLE_CMD_REGEX); 283 m = p.matcher(s); //<t REG CAB SPEED DIR> 284 if (!m.matches()) { 285 p = Pattern.compile(DCCppConstants.THROTTLE_V3_CMD_REGEX); 286 m = p.matcher(s); //<t locoId speed dir> 287 if (!m.matches()) { 288 log.error("Malformed Throttle Command: {}", s); 289 return (null); 290 } 291 int locoId = Integer.parseInt(m.group(1)); 292 int speed = Integer.parseInt(m.group(2)); 293 int dir = Integer.parseInt(m.group(3)); 294 storeLocoSpeedByte(locoId, speed, dir); 295 r = getLocoStateString(locoId); 296 } else { 297 r = "T " + m.group(1) + " " + m.group(3) + " " + m.group(4); 298 } 299 } catch (PatternSyntaxException e) { 300 log.error("Malformed pattern syntax! "); 301 return (null); 302 } catch (IllegalStateException e) { 303 log.error("Group called before match operation executed string= {}", s); 304 return (null); 305 } catch (IndexOutOfBoundsException e) { 306 log.error("Index out of bounds string= {}", s); 307 return (null); 308 } 309 reply = DCCppReply.parseDCCppReply(r); 310 log.debug("Reply generated = '{}'", reply); 311 break; 312 313 case DCCppConstants.FUNCTION_V4_CMD: 314 log.debug("FunctionV4Detected"); 315 s = msg.toString(); 316 r = ""; 317 try { 318 p = Pattern.compile(DCCppConstants.FUNCTION_V4_CMD_REGEX); 319 m = p.matcher(s); //<F locoId func 1|0> 320 if (!m.matches()) { 321 log.error("Malformed FunctionV4 Command: {}", s); 322 return (null); 323 } 324 int locoId = Integer.parseInt(m.group(1)); 325 int fn = Integer.parseInt(m.group(2)); 326 int state = Integer.parseInt(m.group(3)); 327 storeLocoFunction(locoId, fn, state); 328 r = getLocoStateString(locoId); 329 } catch (PatternSyntaxException e) { 330 log.error("Malformed pattern syntax!"); 331 return (null); 332 } catch (IllegalStateException e) { 333 log.error("Group called before match operation executed string= {}", s); 334 return (null); 335 } catch (IndexOutOfBoundsException e) { 336 log.error("Index out of bounds string= {}", s); 337 return (null); 338 } 339 reply = DCCppReply.parseDCCppReply(r); 340 log.debug("Reply generated = '{}'", reply); 341 break; 342 343 case DCCppConstants.TURNOUT_CMD: 344 if (msg.isTurnoutAddMessage() 345 || msg.isTurnoutAddDCCMessage() 346 || msg.isTurnoutAddServoMessage() 347 || msg.isTurnoutAddVpinMessage()) { 348 log.debug("Add Turnout Message"); 349 s = "H" + msg.toString().substring(1) + " 0"; //T reply is H, init to closed 350 turnouts.put(msg.getTOIDInt(), s); 351 r = "O"; 352 } else if (msg.isTurnoutDeleteMessage()) { 353 log.debug("Delete Turnout Message"); 354 turnouts.remove(msg.getTOIDInt()); 355 r = "O"; 356 } else if (msg.isListTurnoutsMessage()) { 357 log.debug("List Turnouts Message"); 358 generateTurnoutListReply(); 359 break; 360 } else if (msg.isTurnoutCmdMessage()) { 361 log.debug("Turnout Command Message"); 362 s = turnouts.get(msg.getTOIDInt()); //retrieve the stored turnout def 363 if (s != null) { 364 s = s.substring(0, s.length()-1) + msg.getTOStateInt(); //replace the last char with new state 365 turnouts.put(msg.getTOIDInt(), s); //update the stored turnout 366 r = "H " + msg.getTOIDString() + " " + msg.getTOStateInt(); 367 } else { 368 log.warn("Unknown turnout ID '{}'", msg.getTOIDInt()); 369 r = "X"; 370 } 371 372 } else { 373 log.debug("Unknown TURNOUT_CMD detected"); 374 r = "X"; 375 } 376 reply = DCCppReply.parseDCCppReply(r); 377 log.debug("Reply generated = '{}'", reply); 378 break; 379 380 case DCCppConstants.OUTPUT_CMD: 381 if (msg.isOutputCmdMessage()) { 382 log.debug("Output Command Message: '{}'", msg); 383 s = turnouts.get(msg.getOutputIDInt()); //retrieve the stored turnout def 384 if (s != null) { 385 s = s.substring(0, s.length()-1) + (msg.getOutputStateBool() ? "1" : "0"); //replace the last char with new state 386 turnouts.put(msg.getOutputIDInt(), s); //update the stored turnout 387 r = "Y " + msg.getOutputIDInt() + " " + (msg.getOutputStateBool() ? "1" : "0"); 388 reply = DCCppReply.parseDCCppReply(r); 389 log.debug("Reply generated = {}", reply.toString()); 390 } else { 391 log.warn("Unknown output ID '{}'", msg.getOutputIDInt()); 392 r = "X"; 393 } 394 } else if (msg.isOutputAddMessage()) { 395 log.debug("Output Add Message"); 396 s = "Y" + msg.toString().substring(1) + " 0"; //Z reply is Y, init to closed 397 turnouts.put(msg.getOutputIDInt(), s); 398 r = "O"; 399 } else if (msg.isOutputDeleteMessage()) { 400 log.debug("Output Delete Message"); 401 turnouts.remove(msg.getOutputIDInt()); 402 r = "O"; 403 } else if (msg.isListOutputsMessage()) { 404 log.debug("Output List Message"); 405 generateTurnoutListReply(); 406 break; 407 } else { 408 log.error("Unknown Output Command: '{}'", msg.toString()); 409 r = "X"; 410 } 411 reply = DCCppReply.parseDCCppReply(r); 412 log.debug("Reply generated = '{}'", reply); 413 break; 414 415 case DCCppConstants.SENSOR_CMD: 416 if (msg.isSensorAddMessage()) { 417 log.debug("SENSOR_CMD Add detected"); 418 //s = msg.toString(); 419 r = "O"; // TODO: Randomize? 420 } else if (msg.isSensorDeleteMessage()) { 421 log.debug("SENSOR_CMD Delete detected"); 422 //s = msg.toString(); 423 r = "O"; // TODO: Randomize? 424 } else if (msg.isListSensorsMessage()) { 425 r = "Q 1 4 1"; // TODO: DO this for real. 426 } else { 427 log.debug("Invalid SENSOR_CMD detected"); 428 r = "X"; 429 } 430 reply = DCCppReply.parseDCCppReply(r); 431 log.debug("Reply generated = '{}'", reply); 432 break; 433 434 case DCCppConstants.PROG_WRITE_CV_BYTE: 435 log.debug("PROG_WRITE_CV_BYTE detected"); 436 s = msg.toString(); 437 r = ""; 438 try { 439 if (s.matches(DCCppConstants.PROG_WRITE_BYTE_REGEX)) { 440 p = Pattern.compile(DCCppConstants.PROG_WRITE_BYTE_REGEX); 441 m = p.matcher(s); 442 if (!m.matches()) { 443 log.error("Malformed ProgWriteCVByte Command: {}", s); 444 return (null); 445 } 446 // CMD: <W CV Value CALLBACKNUM CALLBACKSUB> 447 // Response: <r CALLBACKNUM|CALLBACKSUB|CV Value> 448 r = "r " + m.group(3) + "|" + m.group(4) + "|" + m.group(1) + 449 " " + m.group(2); 450 CVs[Integer.parseInt(m.group(1))] = Integer.parseInt(m.group(2)); 451 } else if (s.matches(DCCppConstants.PROG_WRITE_BYTE_V4_REGEX)) { 452 p = Pattern.compile(DCCppConstants.PROG_WRITE_BYTE_V4_REGEX); 453 m = p.matcher(s); 454 if (!m.matches()) { 455 log.error("Malformed ProgWriteCVByte Command: {}", s); 456 return (null); 457 } 458 // CMD: <W CV Value> 459 // Response: <r CV Value> 460 r = "r " + m.group(1) + " " + m.group(2); 461 CVs[Integer.parseInt(m.group(1))] = Integer.parseInt(m.group(2)); 462 } 463 reply = DCCppReply.parseDCCppReply(r); 464 log.debug("Reply generated = {}", reply.toString()); 465 } catch (PatternSyntaxException e) { 466 log.error("Malformed pattern syntax!"); 467 return (null); 468 } catch (IllegalStateException e) { 469 log.error("Group called before match operation executed string= {}", s); 470 return (null); 471 } catch (IndexOutOfBoundsException e) { 472 log.error("Index out of bounds string= {}", s); 473 return (null); 474 } 475 break; 476 477 case DCCppConstants.PROG_WRITE_CV_BIT: 478 log.debug("PROG_WRITE_CV_BIT detected"); 479 s = msg.toString(); 480 try { 481 p = Pattern.compile(DCCppConstants.PROG_WRITE_BIT_REGEX); 482 m = p.matcher(s); 483 if (!m.matches()) { 484 log.error("Malformed ProgWriteCVBit Command: {}", s); 485 return (null); 486 } 487 // CMD: <B CV BIT Value CALLBACKNUM CALLBACKSUB> 488 // Response: <r CALLBACKNUM|CALLBACKSUB|CV BIT Value> 489 r = "r " + m.group(4) + "|" + m.group(5) + "|" + m.group(1) + " " 490 + m.group(2) + m.group(3); 491 int idx = Integer.parseInt(m.group(1)); 492 int bit = Integer.parseInt(m.group(2)); 493 int v = Integer.parseInt(m.group(3)); 494 if (v == 1) { 495 CVs[idx] = CVs[idx] | (0x0001 << bit); 496 } else { 497 CVs[idx] = CVs[idx] & ~(0x0001 << bit); 498 } 499 reply = DCCppReply.parseDCCppReply(r); 500 log.debug("Reply generated = {}", reply.toString()); 501 } catch (PatternSyntaxException e) { 502 log.error("Malformed pattern syntax!"); 503 return (null); 504 } catch (IllegalStateException e) { 505 log.error("Group called before match operation executed string= {}", s); 506 return (null); 507 } catch (IndexOutOfBoundsException e) { 508 log.error("Index out of bounds string= {}", s); 509 return (null); 510 } 511 break; 512 513 case DCCppConstants.PROG_READ_CV: 514 log.debug("PROG_READ_CV detected"); 515 s = msg.toString(); 516 r = ""; 517 try { 518 if (s.matches(DCCppConstants.PROG_READ_CV_REGEX)) { 519 p = Pattern.compile(DCCppConstants.PROG_READ_CV_REGEX); 520 m = p.matcher(s); 521 int cv = Integer.parseInt(m.group(1)); 522 int cvVal = 0; // Default to 0 if they're reading out of bounds. 523 if (cv < CVs.length) { 524 cvVal = CVs[Integer.parseInt(m.group(1))]; 525 } 526 // CMD: <R CV CALLBACKNUM CALLBACKSUB> 527 // Response: <r CALLBACKNUM|CALLBACKSUB|CV Value> 528 r = "r " + m.group(2) + "|" + m.group(3) + "|" + m.group(1) + " " 529 + cvVal; 530 } else if (s.matches(DCCppConstants.PROG_READ_CV_V4_REGEX)) { 531 p = Pattern.compile(DCCppConstants.PROG_READ_CV_V4_REGEX); 532 m = p.matcher(s); 533 if (!m.matches()) { 534 log.error("Malformed PROG_READ_CV Command: {}", s); 535 return (null); 536 } 537 int cv = Integer.parseInt(m.group(1)); 538 int cvVal = 0; // Default to 0 if they're reading out of bounds. 539 if (cv < CVs.length) { 540 cvVal = CVs[Integer.parseInt(m.group(1))]; 541 } 542 // CMD: <R CV> 543 // Response: <r CV Value> 544 r = "r " + m.group(1) + " " + cvVal; 545 } else if (s.matches(DCCppConstants.PROG_READ_LOCOID_REGEX)) { 546 int locoId = ThreadLocalRandom.current().nextInt(9999)+1; //get a random locoId between 1 and 9999 547 // CMD: <R> 548 // Response: <r LocoId> 549 r = "r " + locoId; 550 } else { 551 log.error("Malformed PROG_READ_CV Command: {}", s); 552 return (null); 553 } 554 555 reply = DCCppReply.parseDCCppReply(r); 556 log.debug("Reply generated = {}", reply.toString()); 557 } catch (PatternSyntaxException e) { 558 log.error("Malformed pattern syntax!"); 559 return (null); 560 } catch (IllegalStateException e) { 561 log.error("Group called before match operation executed string= {}", s); 562 return (null); 563 } catch (IndexOutOfBoundsException e) { 564 log.error("Index out of bounds string= {}", s); 565 return (null); 566 } 567 break; 568 569 case DCCppConstants.PROG_VERIFY_CV: 570 log.debug("PROG_VERIFY_CV detected"); 571 s = msg.toString(); 572 try { 573 p = Pattern.compile(DCCppConstants.PROG_VERIFY_REGEX); 574 m = p.matcher(s); 575 if (!m.matches()) { 576 log.error("Malformed PROG_VERIFY_CV Command: {}", s); 577 return (null); 578 } 579 // TODO: Work Magic Here to retrieve stored value. 580 // Make sure that CV exists 581 int cv = Integer.parseInt(m.group(1)); 582 int cvVal = 0; // Default to 0 if they're reading out of bounds. 583 if (cv < CVs.length) { 584 cvVal = CVs[cv]; 585 } 586 // CMD: <V CV STARTVAL> 587 // Response: <v CV Value> 588 r = "v " + cv + " " + cvVal; 589 590 reply = DCCppReply.parseDCCppReply(r); 591 log.debug("Reply generated = {}", reply.toString()); 592 } catch (PatternSyntaxException e) { 593 log.error("Malformed pattern syntax!"); 594 return (null); 595 } catch (IllegalStateException e) { 596 log.error("Group called before match operation executed string= {}", s); 597 return (null); 598 } catch (IndexOutOfBoundsException e) { 599 log.error("Index out of bounds string= {}", s); 600 return (null); 601 } 602 break; 603 604 case DCCppConstants.TRACK_POWER_ON: 605 log.debug("TRACK_POWER_ON detected"); 606 trackPowerState = true; 607 reply = DCCppReply.parseDCCppReply("p1"); 608 break; 609 610 case DCCppConstants.TRACK_POWER_OFF: 611 log.debug("TRACK_POWER_OFF detected"); 612 trackPowerState = false; 613 reply = DCCppReply.parseDCCppReply("p0"); 614 break; 615 616 case DCCppConstants.READ_MAXNUMSLOTS: 617 log.debug("READ_MAXNUMSLOTS detected"); 618 reply = DCCppReply.parseDCCppReply("# 12"); 619 break; 620 621 case DCCppConstants.READ_TRACK_CURRENT: 622 log.debug("READ_TRACK_CURRENT detected"); 623 generateMeterReplies(); 624 break; 625 626 case DCCppConstants.TRACKMANAGER_CMD: 627 log.debug("TRACKMANAGER_CMD detected"); 628 reply = DCCppReply.parseDCCppReply("= A MAIN"); 629 writeReply(reply); 630 reply = DCCppReply.parseDCCppReply("= B PROG"); 631 writeReply(reply); 632 reply = DCCppReply.parseDCCppReply("= C MAIN"); 633 writeReply(reply); 634 reply = DCCppReply.parseDCCppReply("= D MAIN"); 635 break; 636 637 case DCCppConstants.LCD_TEXT_CMD: 638 log.debug("LCD_TEXT_CMD detected"); 639 DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss a"); 640 LocalDateTime now = LocalDateTime.now(); 641 String dateTimeString = now.format(formatter); 642 reply = DCCppReply.parseDCCppReply("@ 0 0 \"Welcome to DCC-EX -- " + dateTimeString + "\"" ); 643 writeReply(reply); 644 reply = DCCppReply.parseDCCppReply("@ 0 1 \"LCD Line 1\""); 645 writeReply(reply); 646 reply = DCCppReply.parseDCCppReply("@ 0 2 \"LCD Line 2\""); 647 writeReply(reply); 648 reply = DCCppReply.parseDCCppReply("@ 0 3 \" LCD Line 3 with spaces \""); 649 writeReply(reply); 650 reply = DCCppReply.parseDCCppReply("@ 0 4 \"1234567890123456789012345678901234567890\""); 651 break; 652 653 case DCCppConstants.READ_CS_STATUS: 654 log.debug("READ_CS_STATUS detected"); 655 generateReadCSStatusReply(); // Handle this special. 656 break; 657 658 case DCCppConstants.THROTTLE_COMMANDS: 659 log.debug("THROTTLE_COMMANDS detected"); 660 if (msg.isCurrentMaxesMessage()) { 661 reply = DCCppReply.parseDCCppReply("jG 4998 4998 4998 4998"); 662 } else if (msg.isCurrentValuesMessage()) { 663 generateCurrentValuesReply(); // Handle this special. 664 } 665 break; 666 667 case DCCppConstants.FUNCTION_CMD: 668 case DCCppConstants.FORGET_CAB_CMD: 669 case DCCppConstants.ACCESSORY_CMD: 670 case DCCppConstants.OPS_WRITE_CV_BYTE: 671 case DCCppConstants.OPS_WRITE_CV_BIT: 672 case DCCppConstants.WRITE_DCC_PACKET_MAIN: 673 case DCCppConstants.WRITE_DCC_PACKET_PROG: 674 log.debug("non-reply message detected: '{}'", msg); 675 // Send no reply. 676 return (null); 677 678 default: 679 log.debug("unknown message detected: '{}'", msg); 680 return (null); 681 } 682 return (reply); 683 } 684 685 //calc speedByte value matching DCC++EX, then store it, so it can be used in the locoState replies 686 private void storeLocoSpeedByte(int locoId, int speed, int dir) { 687 if (speed>0) speed++; //add 1 to speed if not zero or estop 688 if (speed<0) speed = 1; //eStop is actually 1 689 int dirBit = dir*128; //calc value for direction bit 690 int speedByte = dirBit + speed; //add dirBit to adjusted speed value 691 locoSpeedByte.put(locoId, speedByte); //store it 692 if (!locoFunctions.containsKey(locoId)) locoFunctions.put(locoId, 0); //init functions if not set 693 } 694 695 //stores the calculated value of the functionsByte as used by DCC++EX 696 private void storeLocoFunction(int locoId, int function, int state) { 697 int functions = 0; //init functions to all off if not stored 698 if (locoFunctions.containsKey(locoId)) 699 functions = locoFunctions.get(locoId); //get stored value, if any 700 int mask = 1 << function; 701 if (state == 1) { 702 functions = functions | mask; //apply ON 703 } else { 704 functions = functions & ~mask; //apply OFF 705 } 706 locoFunctions.put(locoId, functions); //store new value 707 if (!locoSpeedByte.containsKey(locoId)) 708 locoSpeedByte.put(locoId, 0); //init speedByte if not set 709 } 710 711 //retrieve stored values and calculate and format the locostate message text 712 private String getLocoStateString(int locoId) { 713 String s; 714 int speedByte = locoSpeedByte.get(locoId); 715 int functions = locoFunctions.get(locoId); 716 s = "l " + locoId + " 0 " + speedByte + " " + functions; //<l loco slot speedByte functions> 717 return s; 718 } 719 720 /* 's'tatus message gets multiple reply messages */ 721 private void generateReadCSStatusReply() { 722 DCCppReply r = new DCCppReply("p" + (trackPowerState ? "1" : "0")); 723 writeReply(r); 724 r = DCCppReply.parseDCCppReply("iDCC-EX V-5.0.4 / MEGA / STANDARD_MOTOR_SHIELD G-9db6d36"); 725 writeReply(r); 726 generateTurnoutStatesReply(); 727 } 728 729 /* Send list of creation command with states for all defined turnouts and outputs */ 730 private void generateTurnoutListReply() { 731 if (!turnouts.isEmpty()) { 732 turnouts.forEach((key, value) -> { //send back the full create string for each 733 DCCppReply r = new DCCppReply(value); 734 writeReply(r); 735 }); 736 } else { 737 writeReply(new DCCppReply("X No Turnouts Defined")); 738 } 739 } 740 741 /* Send list of turnout states */ 742 private void generateTurnoutStatesReply() { 743 if (!turnouts.isEmpty()) { 744 turnouts.forEach((key, value) -> { 745 String s = value.substring(0,2) + key + value.substring(value.length()-2); //command char + id + state 746 DCCppReply r = new DCCppReply(s); 747 writeReply(r); 748 }); 749 } else { 750 writeReply(new DCCppReply("X No Turnouts Defined")); 751 } 752 } 753 754 /* 'c' current request message gets multiple reply messages */ 755 private void generateMeterReplies() { 756 int currentmA = 1100 + ThreadLocalRandom.current().nextInt(64); 757 double voltageV = 14.5 + ThreadLocalRandom.current().nextInt(10)/10.0; 758 String rs = "c CurrentMAIN " + (trackPowerState ? Double.toString(currentmA) : "0") + " C Milli 0 1997 1 1997"; 759 DCCppReply r = new DCCppReply(rs); 760 writeReply(r); 761 r = new DCCppReply("c VoltageMAIN " + voltageV + " V NoPrefix 0 18.0 0.1 16.0"); 762 writeReply(r); 763 } 764 765 /* 'JI' Current Value List request message returns an array of Current Values */ 766 private void generateCurrentValuesReply() { 767 int currentmA_0 = 1100 + ThreadLocalRandom.current().nextInt(64); 768 int currentmA_1 = 0500 + ThreadLocalRandom.current().nextInt(64); 769 int currentmA_2 = 1100 + ThreadLocalRandom.current().nextInt(64); 770 int currentmA_3 = 1100 + ThreadLocalRandom.current().nextInt(64); 771 String rs = "jI " + (trackPowerState ? Integer.toString(currentmA_0) : "0") + " " + 772 (trackPowerState ? Integer.toString(currentmA_1) : "0") + " " + 773 (trackPowerState ? Integer.toString(currentmA_2) : "0") + " " + 774 (trackPowerState ? Integer.toString(currentmA_3) : "0"); 775 DCCppReply r = new DCCppReply(rs); 776 writeReply(r); 777 } 778 779 private void generateRandomSensorReply() { 780 // Pick a random sensor number between 0 and 10; 781 int sensorNum = ThreadLocalRandom.current().nextInt(10)+1; // Generate a random sensor number between 1 and 10 782 int value = ThreadLocalRandom.current().nextInt(2); // Generate state value between 0 and 1 783 784 String reply = (value == 1 ? "Q " : "q ") + sensorNum; 785 786 DCCppReply r = DCCppReply.parseDCCppReply(reply); 787 writeReply(r); 788 } 789 790 private void writeReply(DCCppReply r) { 791 log.debug("Simulator Thread sending Reply '{}'", r); 792 int i; 793 int len = r.getLength(); // opCode+Nbytes+ECC 794 // If r == null, there is no reply to be sent. 795 try { 796 outpipe.writeByte((byte) '<'); 797 for (i = 0; i < len; i++) { 798 outpipe.writeByte((byte) r.getElement(i)); 799 } 800 outpipe.writeByte((byte) '>'); 801 } catch (java.io.IOException ex) { 802 ConnectionStatus.instance().setConnectionState(getUserName(), getCurrentPortName(), ConnectionStatus.CONNECTION_DOWN); 803 } 804 } 805 806 /** 807 * Get characters from the input source, and file a message. 808 * <p> 809 * Returns only when the message is complete. 810 * <p> 811 * Only used in the Receive thread. 812 * 813 * @return filled message 814 * @throws IOException when presented by the input source. 815 */ 816 private DCCppMessage loadChars() throws java.io.IOException { 817 // Spin waiting for start-of-frame '<' character (and toss it) 818 StringBuilder s = new StringBuilder(); 819 byte char1; 820 boolean found_start = false; 821 822 // this loop reads every other character; is that the desired behavior? 823 while (!found_start) { 824 char1 = readByteProtected(inpipe); 825 if ((char1 & 0xFF) == '<') { 826 found_start = true; 827 log.trace("Found starting < "); 828 break; // A bit redundant with setting the loop condition true (false) 829 } else { 830 // drop next character before repeating 831 readByteProtected(inpipe); 832 } 833 } 834 // Now, suck in the rest of the message... 835 for (int i = 0; i < DCCppConstants.MAX_MESSAGE_SIZE; i++) { 836 char1 = readByteProtected(inpipe); 837 if (char1 == '>') { 838 log.trace("msg found > "); 839 // Don't store the > 840 break; 841 } else { 842 log.trace("msg read byte {}", char1); 843 char c = (char) (char1 & 0x00FF); 844 s.append(c); 845 } 846 } 847 // TODO: Still need to strip leading and trailing whitespace. 848 log.debug("Complete message = {}", s); 849 return (new DCCppMessage(s.toString())); 850 } 851 852 /** 853 * Read a single byte, protecting against various timeouts, etc. 854 * <p> 855 * When a port is set to have a receive timeout (via the 856 * enableReceiveTimeout() method), some will return zero bytes or an 857 * EOFException at the end of the timeout. In that case, the read should be 858 * repeated to get the next real character. 859 * @param istream source of data 860 * @return next available byte, when available 861 * @throws IOException from underlying operation 862 * 863 */ 864 protected byte readByteProtected(DataInputStream istream) throws java.io.IOException { 865 byte[] rcvBuffer = new byte[1]; 866 while (true) { // loop will repeat until character found 867 int nchars; 868 nchars = istream.read(rcvBuffer, 0, 1); 869 if (nchars > 0) { 870 return rcvBuffer[0]; 871 } 872 } 873 } 874 875 volatile static DCCppSimulatorAdapter mInstance = null; 876 private DataOutputStream pout = null; // for output to other classes 877 private DataInputStream pin = null; // for input from other classes 878 // internal ends of the pipes 879 private DataOutputStream outpipe = null; // feed pin 880 private DataInputStream inpipe = null; // feed pout 881 private Thread sourceThread; 882 883 private final static Logger log = LoggerFactory.getLogger(DCCppSimulatorAdapter.class); 884 885}