001package jmri.jmrix.nce.simulator; 002 003import java.io.*; 004import java.util.Arrays; 005 006import org.slf4j.Logger; 007import org.slf4j.LoggerFactory; 008 009import jmri.jmrix.nce.*; 010import jmri.util.ImmediatePipedOutputStream; 011 012/** 013 * The following was copied from the NCE Power Pro System Reference Manual. It 014 * provides the background for the various NCE command that are simulated by 015 * this implementation. 016 * <p> 017 * Implements a Command Station Simulator for the NCE system. 018 * <p> 019 * BINARY COMMAND SET 020 * <p> 021 * The RS-232 port binary commands are designed to work in a computer friendly 022 * way. Command format is: {@code <command number> <data> <data> ...} 023 * <p> 024 * Commands range from 0x80 to 0xBF 025 * <p> 026 * Commands and formats supported: Commands 0xAD to 0xBF are not used and return 027 * '0' 028 * <p> 029 * Errors returned: '0'= command not supported '1'= loco address out of range 030 * '2'= cab address out of range '3'= data out of range '4'= byte count out of 031 * range '!'= command completed successfully 032 * <p> 033 * For a complete description of Binary Commands see: 034 * www.ncecorporation.com/pdf/bincmds.pdf <br> 035 * 036 * <pre>{@literal 037 * Command Description (#bytes rtn) Responses 038 * 0x80 NOP, dummy instruction (1) ! 039 * 0x81 xx xx yy assign loco xxxx to cab cc (1) !, 1,2 040 * 0x82 read clock (2) <hours><minutes> 041 * 0x83 Clock stop (1) ! 042 * 0x84 Clock start (1) ! 043 * 0x85 xx xx Set clock hr./min (1) !,3 044 * 0x86 xx Set clock 12/24 (1) !,3 045 * 0x87 xx Set clock ratio (1) !,3 046 * 0x88 xxxx Dequeue packet by loco addr (1) !, 1,2 047 * 0x89 Enable main trk, kill prog (1) ! 048 * 0x8A yy Return status of AIU yy (4) <current hi byte> <current lo byte> <change hi byte> <change lo byte> 049 * 0x8B Kill main trk, enable prog (1) ! 050 * 0x8C dummy inst. returns "!" followed CR/LF(3) !, 0x0D, 0x0A 051 * 0x8D xxxx mm Set speed mode of loco xxxx to mode mm, 1=14, 2=28, 3=128 (1) !, 1,3<speed mode, 0 to 3> 052 * 0x8E aaaa nn<16 data bytes> Write nn bytes, start at aaaa Must have 16 data bytes, pad them out to 16 if necessary (1) !,4 053 * 0x8F aaaa Read 16 bytes, start at aaaa(16) 16 bytes 054 * 0x90 cc xx... Send 16 char message to Cab ccLCD line 3. xx = 16 ASCII char (1) ! ,2 055 * 0x91 cc xx Send 16 char message to cab cc LCD line 4. xx=16 ASCII (1) !,2 056 * 0x92 cc xx Send 8 char message to cab cc LCD line 2 right xx=8 char (1) !,2 057 * 0x93 ss<3 byte packet> Queue 3 byte packet to temp _Q send ss times (1) ! 058 * 0x94 ss<4 byte packet> Queue 4 byte packet to temp _Q send ss times (1) ! 059 * 0x95 ss<5 byte packet> Queue 5 byte packet to temp_Q send ss times (1) ! 060 * 0x96 ss<6 byte packet> Queue 6 byte packet to temp _Q send ss times (1) ! 061 * 0x97 aaaa xx Write 1 byte to aaaa (1) ! 062 * 0x98 aaaa xx xxWrite 2 bytes to aaaa (1) ! 063 * 0x99 aaaa<4 data bytes> Write 4 bytes to aaaa (1) ! 064 * 0x9A aaaa<8 data bytes> Write 8 bytes to aaaa (1) ! 065 * 0x9B yy Return status of AIU yy (short form of command 0x8A) (2) <current hi byte><current lo byte><br> 066 * 0x9C xx Execute macro number xx (1) !, 0,3 067 * 0x9D aaaa Read 1 byte from aaaa (1) 1 byte 068 * 0x9E Enter programming track mode(1) !=success 3=short circuit 069 * 0x9F Exit programming track mode (1) !=success 070 * 0xA0 aaaa xx Program CV aa with data xx in paged mode (1) !=success 0=program track 071 * 0xA1 aaaa Read CV aaaa in paged mode Note: cv data followed by ! for OK. 0xFF followed by 3 for can't read CV (2) !, 0,3 072 * 0xA2<4 data bytes> Locomotive control command (1) !,1 073 * 0xA3<3 bytepacket> Queue 3 byte packet to TRK _Q (replaces any packet with same address if it exists) (1) !,1 074 * 0xA4<4 byte packet> Queue 4 byte packet to TRK _Q (1) !,1 075 * 0xA5<5 byte packet> Queue 5 byte packet to TRK _Q (1) !,1 076 * 0xA6 rr dd Program register rr with dd (1) !=success 0=no program track 077 * 0xA7 rr Read register rr. Note: cv data followed by ! for OK. 0xFF followed by 3 for can't read CV (2) !,3 0=no program track 078 * 0xA8 aaaa dd Program CV aaaa with dd in direct mode. (1) !=success 0=no program track 079 * 0xA9 aaaa Read CV aaaa in direct mode. Note: cv data followed by ! for OK. 080 * 0xFF followed by 3 for can't read CV (2) !,3 081 * 0xAA Return software revision number. Format: VV.MM.mm (3) 3 data bytes 082 * 0xAB Perform soft reset of command station (like cycling power) (0) Returns nothing 083 * 0xAC Perform hard reset of command station. Reset to factory defaults (Note: will change baud rate to 9600)(0) Returns nothing 084 * 0xAD <4 data bytes>Accy/signal and macro commands (1) !,1 085 * }</pre> 086 * 087 * @author Bob Jacobsen Copyright (C) 2001, 2002 088 * @author Paul Bender, Copyright (C) 2009 089 * @author Daniel Boudreau Copyright (C) 2010, 2024 090 * @author Ken Cameron Copyright (C) 2023 091 */ 092public class SimulatorAdapter extends NcePortController implements Runnable { 093 094 NceTrafficController tc; 095 // private control members 096 private Thread sourceThread; 097 098 // streams to share with user class 099 private DataOutputStream pout = null; // this is provided to classes who want to write to us 100 private DataInputStream pin = null; // this is provided to classes who want data from us 101 102 // internal ends of the pipes 103 private DataOutputStream outpipe = null; // feed pin 104 private DataInputStream inpipe = null; // feed pout 105 106 // Simulator responses 107 char NCE_OKAY = '!'; 108 char NCE_ERROR = '0'; 109 char NCE_LOCO_OUT_OF_RANGE = '1'; 110 char NCE_CAB_OUT_OF_RANGE = '2'; 111 char NCE_DATA_OUT_OF_RANGE = '3'; 112 char NCE_BYTE_OUT_OF_RANGE = '4'; 113 114 /** 115 * Create a new SimulatorAdapter. 116 */ 117 public SimulatorAdapter() { 118 super(new NceSystemConnectionMemo()); 119 option1Name = "Eprom"; // NOI18N 120 options.put(option1Name, new Option(Bundle.getMessage("EpromLabel"), option1Values, false)); 121 setOptionState(option1Name, option1Values[1]); 122 } 123 124 /** 125 * {@inheritDoc} Simulated input/output pipes. 126 */ 127 @Override 128 public String openPort(String portName, String appName) { 129 try { 130 PipedOutputStream tempPipeI = new ImmediatePipedOutputStream(); 131 pout = new DataOutputStream(tempPipeI); 132 inpipe = new DataInputStream(new PipedInputStream(tempPipeI)); 133 PipedOutputStream tempPipeO = new ImmediatePipedOutputStream(); 134 outpipe = new DataOutputStream(tempPipeO); 135 pin = new DataInputStream(new PipedInputStream(tempPipeO)); 136 } catch (java.io.IOException e) { 137 log.error("{}: init (pipe): Exception: ", manufacturerName, e); 138 } 139 opened = true; 140 return null; // indicates OK return 141 } 142 143 // Warning: EPROM revision must match option1Values array index value. 144 String[] option1Values = new String[]{"2006 to Mar 1 2007", "Mar 3 2007 to Jan 24 2008", "Feb 2 2008 to Jan 30 2021", "Feb 22 2021 on"}; // NOI18N 145 int epromRevision = -1; 146 147 /** 148 * Set up all of the other objects to simulate operation with an NCE command 149 * station. 150 */ 151 @Override 152 public void configure() { 153 tc = new NceTrafficController(); 154 this.getSystemConnectionMemo().setNceTrafficController(tc); 155 tc.setAdapterMemo(this.getSystemConnectionMemo()); 156 157 // setting binary mode 158 this.getSystemConnectionMemo().configureCommandStation(NceTrafficController.OPTION_2006); 159 tc.setCmdGroups(NceTrafficController.CMDS_MEM 160 | NceTrafficController.CMDS_AUI_READ 161 | NceTrafficController.CMDS_PROGTRACK 162 | NceTrafficController.CMDS_OPS_PGM 163 | NceTrafficController.CMDS_USB 164 | NceTrafficController.CMDS_NOT_USB 165 | NceTrafficController.CMDS_CLOCK 166 | NceTrafficController.CMDS_ALL_SYS); 167 tc.setUsbSystem(NceTrafficController.USB_SYSTEM_NONE); 168 169 epromRevision = Arrays.asList(option1Values).indexOf(getOptionState(option1Name)); 170 if (epromRevision == -1) { 171 epromRevision = 1; // default revision if no match 172 } 173 174 tc.csm = new NceCmdStationMemory(); 175 tc.connectPort(this); 176 tc.setSimulatorRunning(true); 177 178 this.getSystemConnectionMemo().configureManagers(); 179 180 // initialize memory pools 181 turnoutMemory = new byte[tc.csm.getAccyMemSize()]; 182 consistMemory = new byte[tc.csm.getConsistMidSize() + 16]; //needs some padding 183 macroMemory = new byte[tc.csm.getMacroLimit() * tc.csm.getMacroSize()]; 184 185 // start the simulator 186 sourceThread = new Thread(this); 187 sourceThread.setName("Nce Simulator"); 188 sourceThread.setPriority(Thread.MIN_PRIORITY); 189 sourceThread.start(); 190 } 191 192 // Base class methods for the NcePortController interface. 193 /** 194 * {@inheritDoc} 195 */ 196 @Override 197 public DataInputStream getInputStream() { 198 if (!opened || pin == null) { 199 log.error("getInputStream called before load(), stream not available"); 200 } 201 return pin; 202 } 203 204 /** 205 * {@inheritDoc} 206 */ 207 @Override 208 public DataOutputStream getOutputStream() { 209 if (!opened || pout == null) { 210 log.error("getOutputStream called before load(), stream not available"); 211 } 212 return pout; 213 } 214 215 /** 216 * {@inheritDoc} 217 */ 218 @Override 219 public boolean status() { 220 return opened; 221 } 222 223 /** 224 * {@inheritDoc} 225 * 226 * @return null 227 */ 228 @Override 229 public String[] validBaudRates() { 230 log.debug("validBaudRates should not have been invoked"); 231 return new String[]{}; 232 } 233 234 /** 235 * {@inheritDoc} 236 * 237 * @return null 238 */ 239 @Override 240 public int[] validBaudNumbers() { 241 return new int[]{}; 242 } 243 244 @Override 245 public String getCurrentBaudRate() { 246 return ""; 247 } 248 249 @Override 250 public String getCurrentPortName() { 251 return ""; 252 } 253 254 @Override 255 public void run() { // start a new thread 256 // This thread has one task. It repeatedly reads from the input pipe 257 // and writes an appropriate response to the output pipe. This is the heart 258 // of the NCE command station simulation. 259 // report status? 260 log.info("NCE Simulator Started"); 261 while (true) { 262 NceMessage m = readMessage(); 263 if (log.isDebugEnabled()) { 264 StringBuilder buf = new StringBuilder(); 265 for (int i = 0; i < m.getNumDataElements(); i++) { 266 buf.append(Integer.toHexString(0xFF & m.getElement(i)).toUpperCase()).append(" "); 267 } 268 log.debug("Nce simulator received message: {}", buf ); 269 } 270 if (m != null) { 271 NceReply r = generateReply(m); 272 writeReply(r); 273 if (log.isDebugEnabled() && r != null) { 274 StringBuilder buf = new StringBuilder(); 275 for (int i = 0; i < r.getNumDataElements(); i++) { 276 buf.append(Integer.toHexString(0xFF & r.getElement(i)).toUpperCase()).append(" "); 277 } 278 log.debug("Nce simulator sent reply: {}", buf.toString()); 279 } 280 } 281 } 282 } 283 284 // readMessage reads one incoming message from the buffer 285 private NceMessage readMessage() { 286 NceMessage msg = null; 287 try { 288 msg = loadChars(); 289 } catch (java.io.IOException e) { 290 291 } 292 return (msg); 293 } 294 295 /** 296 * Get characters from the input source. 297 * 298 * @return filled message 299 * @throws IOException when presented by the input source. 300 */ 301 private NceMessage loadChars() throws java.io.IOException { 302 int nchars; 303 byte[] rcvBuffer = new byte[32]; 304 305 nchars = inpipe.read(rcvBuffer, 0, 32); 306 //log.debug("new message received"); 307 NceMessage msg = new NceMessage(nchars); 308 309 for (int i = 0; i < nchars; i++) { 310 msg.setElement(i, rcvBuffer[i] & 0xFF); 311 } 312 return msg; 313 } 314 315 /** 316 * This is the heart of the simulation. It translates an incoming NceMessage 317 * into an outgoing NceReply. 318 */ 319 private NceReply generateReply(NceMessage m) { 320 NceReply reply = new NceReply(this.getSystemConnectionMemo().getNceTrafficController()); 321 int command = m.getElement(0); 322 if (command < 0x80) { // NOTE: NCE command station does not respond to 323 return null; // command less than 0x80 (times out) 324 } 325 if (command > 0xBF) { // Command is out of range 326 reply.setElement(0, NCE_ERROR); // Nce command not supported 327 return reply; 328 } 329 switch (command) { 330 case NceMessage.SW_REV_CMD: // Get EPROM revision 331 reply.setElement(0, 0x06); // Send EPROM revision based on Preference setting 332 reply.setElement(1, 0x02); 333 reply.setElement(2, epromRevision); 334 break; 335 case NceMessage.READ_CLOCK_CMD: // Read clock 336 reply.setElement(0, 0x12); // Return fixed time 337 reply.setElement(1, 0x30); 338 break; 339 case NceMessage.READ_AUI4_CMD: // Read AUI 4 byte response 340 reply.setElement(0, 0xFF); // fixed data for now 341 reply.setElement(1, 0xFF); // fixed data for now 342 reply.setElement(2, 0x00); // fixed data for now 343 reply.setElement(3, 0x00); // fixed data for now 344 break; 345 case NceMessage.DUMMY_CMD: // Dummy instruction 346 reply.setElement(0, NCE_OKAY); // return ! CR LF 347 reply.setElement(1, 0x0D); 348 reply.setElement(2, 0x0A); 349 break; 350 case NceMessage.READ16_CMD: // Read 16 bytes 351 readMemory(m, reply, 16); 352 break; 353 case NceMessage.READ_AUI2_CMD: // Read AUI 2 byte response 354 reply.setElement(0, 0x00); // fixed data for now 355 reply.setElement(1, 0x00); // fixed data for now 356 break; 357 case NceMessage.READ1_CMD: // Read 1 bytes 358 readMemory(m, reply, 1); 359 break; 360 case NceMessage.WRITE1_CMD: // Write 1 bytes 361 writeMemory(m, reply, 1, false); 362 break; 363 case NceMessage.WRITE2_CMD: // Write 2 bytes 364 writeMemory(m, reply, 2, false); 365 break; 366 case NceMessage.WRITE4_CMD: // Write 4 bytes 367 writeMemory(m, reply, 4, false); 368 break; 369 case NceMessage.WRITE8_CMD: // Write 8 bytes 370 writeMemory(m, reply, 8, false); 371 break; 372 case NceMessage.WRITE_N_CMD: // Write n bytes 373 writeMemory(m, reply, m.getElement(3), true); 374 break; 375 case NceMessage.SEND_ACC_SIG_MACRO_CMD: // accessory command 376 accessoryCommand(m, reply); 377 break; 378 case NceMessage.READ_DIR_CV_CMD: 379 case NceMessage.READ_PAGED_CV_CMD: 380 case NceMessage.READ_REG_CMD: 381 reply.setElement(0, 123); // dummy data 382 reply.setElement(1, NCE_OKAY); // forces succeed 383 // Sample code to modify simulator response for testing purposes. 384 // Uncomment and modify as desired. 385// int cvnum = (m.getElement(1) << 8) | (m.getElement(2)); 386// if (cvnum == 7) { 387// reply.setElement(0, 88); // forces fail 388// } 389// if (cvnum == 8) { 390// reply.setElement(0, 48); // forces Hornby 391// } 392// if (cvnum == 159) { 393// reply.setElement(0, 145); 394// reply.setElement(1, NCE_DATA_OUT_OF_RANGE); // forces fail 395// } 396 break; 397 default: 398 reply.setElement(0, NCE_OKAY); // Nce okay reply! 399 } 400 return reply; 401 } 402 403 /** 404 * Write reply to output. 405 * 406 * @param r reply on message 407 */ 408 private void writeReply(NceReply r) { 409 if (r == null) { 410 return; 411 } 412 for (int i = 0; i < r.getNumDataElements(); i++) { 413 try { 414 outpipe.writeByte((byte) r.getElement(i)); 415 } catch (java.io.IOException ex) { 416 } 417 } 418 try { 419 outpipe.flush(); 420 } catch (java.io.IOException ex) { 421 } 422 } 423 424 byte[] turnoutMemory; 425 byte[] macroMemory; 426 byte[] consistMemory; 427 428 /* Read NCE memory. This implementation simulates reading the NCE 429 * command station memory. There are three memory blocks that are 430 * supported, turnout status, macros, and consists. The turnout status 431 * memory is 256 bytes and starts at memory address 0xEC00. The macro memory 432 * is 256*20 or 5120 bytes and starts at memory address 0xC800 (PH5 0x6000). The consist 433 * memory is 256*6 or 1536 bytes and starts at memory address 0xF500 (PH5 0x4E00). 434 * 435 */ 436 private NceReply readMemory(NceMessage m, NceReply reply, int num) { 437 if (num > 16) { 438 log.error("Nce read memory command was greater than 16"); 439 return null; 440 } 441 int nceMemoryAddress = getNceAddress(m); 442 if (nceMemoryAddress >= tc.csm.getAccyMemAddr() && nceMemoryAddress < tc.csm.getAccyMemAddr() + tc.csm.getAccyMemSize()) { 443 log.debug("Reading turnout memory: {}", Integer.toHexString(nceMemoryAddress)); 444 int offset = m.getElement(2); 445 for (int i = 0; i < num; i++) { 446 reply.setElement(i, turnoutMemory[offset + i]); 447 } 448 return reply; 449 } 450 if (nceMemoryAddress >= tc.csm.getConsistHeadAddr() && nceMemoryAddress < tc.csm.getConsistHeadAddr() + tc.csm.getConsistMidSize()) { 451 log.debug("Reading consist memory: {}", Integer.toHexString(nceMemoryAddress)); 452 int offset = nceMemoryAddress - tc.csm.getConsistHeadAddr(); 453 454 for (int i = 0; i < num; i++) { 455 reply.setElement(i, consistMemory[offset + i]); 456 } 457 return reply; 458 } 459 if (nceMemoryAddress >= tc.csm.getMacroAddr() && 460 nceMemoryAddress < tc.csm.getMacroAddr() + (tc.csm.getMacroLimit() * tc.csm.getMacroSize())) { 461 log.debug("Reading macro memory: {}", Integer.toHexString(nceMemoryAddress)); 462 int offset = nceMemoryAddress - tc.csm.getMacroAddr(); 463 log.debug("offset: {}", offset); 464 for (int i = 0; i < num; i++) { 465 reply.setElement(i, macroMemory[offset + i]); 466 } 467 return reply; 468 } 469 for (int i = 0; i < num; i++) { 470 reply.setElement(i, 0x00); // default fixed data 471 } 472 return reply; 473 } 474 475 private NceReply writeMemory(NceMessage m, NceReply reply, int num, boolean skipbyte) { 476 if (num > 16) { 477 log.error("Nce write memory command was greater than 16"); 478 return null; 479 } 480 int nceMemoryAddress = getNceAddress(m); 481 int byteDataBegins = 3; 482 if (skipbyte) { 483 byteDataBegins++; 484 } 485 if (nceMemoryAddress >= tc.csm.getAccyMemAddr() && 486 nceMemoryAddress < tc.csm.getAccyMemAddr() + tc.csm.getAccyMemSize()) { 487 log.debug("Writing turnout memory: {}", Integer.toHexString(nceMemoryAddress)); 488 int offset = m.getElement(2); 489 for (int i = 0; i < num; i++) { 490 turnoutMemory[offset + i] = (byte) m.getElement(i + byteDataBegins); 491 } 492 } 493 if (nceMemoryAddress >= tc.csm.getConsistHeadAddr() && 494 nceMemoryAddress < tc.csm.getConsistHeadAddr() + tc.csm.getConsistMidSize()) { 495 log.debug("Writing consist memory: {}", Integer.toHexString(nceMemoryAddress)); 496 int offset = nceMemoryAddress - tc.csm.getConsistHeadAddr(); 497 for (int i = 0; i < num; i++) { 498 consistMemory[offset + i] = (byte) m.getElement(i + byteDataBegins); 499 } 500 } 501 if (nceMemoryAddress >= tc.csm.getMacroAddr() && 502 nceMemoryAddress < tc.csm.getMacroAddr() + (tc.csm.getMacroLimit() * tc.csm.getMacroSize())) { 503 log.debug("Writing macro memory: {}", Integer.toHexString(nceMemoryAddress)); 504 int offset = nceMemoryAddress - tc.csm.getMacroAddr(); 505 log.debug("offset: {}", offset); 506 for (int i = 0; i < num; i++) { 507 macroMemory[offset + i] = (byte) m.getElement(i + byteDataBegins); 508 } 509 } 510 reply.setElement(0, NCE_OKAY); // Nce okay reply! 511 return reply; 512 } 513 514 /** 515 * Extract item address from a message. 516 * 517 * @param m received message 518 * @return address from the message 519 */ 520 private int getNceAddress(NceMessage m) { 521 int addr = m.getElement(1); 522 addr = addr * 256; 523 addr = addr + m.getElement(2); 524 return addr; 525 } 526 527 private NceReply accessoryCommand(NceMessage m, NceReply reply) { 528 if (m.getElement(3) == 0x03 || m.getElement(3) == 0x04) { // 0x03 = close, 0x04 = throw 529 String operation = "close"; 530 if (m.getElement(3) == 0x04) { 531 operation = "throw"; 532 } 533 int nceAccessoryAddress = getNceAddress(m); 534 log.debug("Accessory command {} NT {}", operation, nceAccessoryAddress); 535 if (nceAccessoryAddress > 2044) { 536 log.error("Turnout address greater than 2044, address: {}", nceAccessoryAddress); 537 return null; 538 } 539 int bit = (nceAccessoryAddress - 1) & 0x07; 540 int setMask = 0x01; 541 for (int i = 0; i < bit; i++) { 542 setMask = setMask << 1; 543 } 544 int clearMask = 0x0FFF - setMask; 545 // log.debug("setMask: {} clearMask: {}", Integer.toHexString(setMask), Integer.toHexString(clearMask)); 546 int offset = (nceAccessoryAddress - 1) >> 3; 547 int read = turnoutMemory[offset]; 548 byte write = (byte) (read & clearMask & 0xFF); 549 550 if (operation.equals("close")) { 551 write = (byte) (write + setMask); // set bit if closed 552 } 553 turnoutMemory[offset] = write; 554 // log.debug("wrote: {}", Integer.toHexString(write)); 555 } 556 reply.setElement(0, NCE_OKAY); // Nce okay reply! 557 return reply; 558 } 559 560 private final static Logger log = LoggerFactory.getLogger(SimulatorAdapter.class); 561 562}