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