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