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