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: ", manufacturerName, 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 158 // setting binary mode 159 this.getSystemConnectionMemo().configureCommandStation(NceTrafficController.OPTION_2006); 160 tc.setCmdGroups(NceTrafficController.CMDS_MEM 161 | NceTrafficController.CMDS_AUI_READ 162 | NceTrafficController.CMDS_PROGTRACK 163 | NceTrafficController.CMDS_OPS_PGM 164 | NceTrafficController.CMDS_USB 165 | NceTrafficController.CMDS_NOT_USB 166 | NceTrafficController.CMDS_CLOCK 167 | NceTrafficController.CMDS_ALL_SYS); 168 tc.setUsbSystem(NceTrafficController.USB_SYSTEM_NONE); 169 170 epromRevision = Arrays.asList(option1Values).indexOf(getOptionState(option1Name)); 171 if (epromRevision == -1) { 172 epromRevision = 1; // default revision if no match 173 } 174 175 tc.csm = new NceCmdStationMemory(); 176 tc.connectPort(this); 177 tc.setSimulatorRunning(true); 178 179 this.getSystemConnectionMemo().configureManagers(); 180 181 // start the simulator 182 sourceThread = new Thread(this); 183 sourceThread.setName("Nce Simulator"); 184 sourceThread.setPriority(Thread.MIN_PRIORITY); 185 sourceThread.start(); 186 } 187 188 // Base class methods for the NcePortController interface. 189 /** 190 * {@inheritDoc} 191 */ 192 @Override 193 public DataInputStream getInputStream() { 194 if (!opened || pin == null) { 195 log.error("getInputStream called before load(), stream not available"); 196 } 197 return pin; 198 } 199 200 /** 201 * {@inheritDoc} 202 */ 203 @Override 204 public DataOutputStream getOutputStream() { 205 if (!opened || pout == null) { 206 log.error("getOutputStream called before load(), stream not available"); 207 } 208 return pout; 209 } 210 211 /** 212 * {@inheritDoc} 213 */ 214 @Override 215 public boolean status() { 216 return opened; 217 } 218 219 /** 220 * {@inheritDoc} 221 * 222 * @return null 223 */ 224 @Override 225 public String[] validBaudRates() { 226 log.debug("validBaudRates should not have been invoked"); 227 return new String[]{}; 228 } 229 230 /** 231 * {@inheritDoc} 232 * 233 * @return null 234 */ 235 @Override 236 public int[] validBaudNumbers() { 237 return new int[]{}; 238 } 239 240 @Override 241 public String getCurrentBaudRate() { 242 return ""; 243 } 244 245 @Override 246 public String getCurrentPortName() { 247 return ""; 248 } 249 250 @Override 251 public void run() { // start a new thread 252 // This thread has one task. It repeatedly reads from the input pipe 253 // and writes an appropriate response to the output pipe. This is the heart 254 // of the NCE command station simulation. 255 // report status? 256 log.info("NCE Simulator Started"); 257 while (true) { 258 NceMessage m = readMessage(); 259 if (log.isDebugEnabled()) { 260 StringBuilder buf = new StringBuilder(); 261 for (int i = 0; i < m.getNumDataElements(); i++) { 262 buf.append(Integer.toHexString(0xFF & m.getElement(i)).toUpperCase()).append(" "); 263 } 264 log.debug("Nce simulator received message: {}", buf ); 265 } 266 if (m != null) { 267 NceReply r = generateReply(m); 268 writeReply(r); 269 if (log.isDebugEnabled() && r != null) { 270 StringBuilder buf = new StringBuilder(); 271 for (int i = 0; i < r.getNumDataElements(); i++) { 272 buf.append(Integer.toHexString(0xFF & r.getElement(i)).toUpperCase()).append(" "); 273 } 274 log.debug("Nce simulator sent reply: {}", buf.toString()); 275 } 276 } 277 } 278 } 279 280 // readMessage reads one incoming message from the buffer 281 private NceMessage readMessage() { 282 NceMessage msg = null; 283 try { 284 msg = loadChars(); 285 } catch (java.io.IOException e) { 286 287 } 288 return (msg); 289 } 290 291 /** 292 * Get characters from the input source. 293 * 294 * @return filled message 295 * @throws IOException when presented by the input source. 296 */ 297 private NceMessage loadChars() throws java.io.IOException { 298 int nchars; 299 byte[] rcvBuffer = new byte[32]; 300 301 nchars = inpipe.read(rcvBuffer, 0, 32); 302 //log.debug("new message received"); 303 NceMessage msg = new NceMessage(nchars); 304 305 for (int i = 0; i < nchars; i++) { 306 msg.setElement(i, rcvBuffer[i] & 0xFF); 307 } 308 return msg; 309 } 310 311 /** 312 * This is the heart of the simulation. It translates an incoming NceMessage 313 * into an outgoing NceReply. 314 */ 315 private NceReply generateReply(NceMessage m) { 316 NceReply reply = new NceReply(this.getSystemConnectionMemo().getNceTrafficController()); 317 int command = m.getElement(0); 318 if (command < 0x80) { // NOTE: NCE command station does not respond to 319 return null; // command less than 0x80 (times out) 320 } 321 if (command > 0xBF) { // Command is out of range 322 reply.setElement(0, NCE_ERROR); // Nce command not supported 323 return reply; 324 } 325 switch (command) { 326 case NceMessage.SW_REV_CMD: // Get EPROM revision 327 reply.setElement(0, 0x06); // Send EPROM revision based on Preference setting 328 reply.setElement(1, 0x02); 329 reply.setElement(2, epromRevision); 330 break; 331 case NceMessage.READ_CLOCK_CMD: // Read clock 332 reply.setElement(0, 0x12); // Return fixed time 333 reply.setElement(1, 0x30); 334 break; 335 case NceMessage.READ_AUI4_CMD: // Read AUI 4 byte response 336 reply.setElement(0, 0xFF); // fixed data for now 337 reply.setElement(1, 0xFF); // fixed data for now 338 reply.setElement(2, 0x00); // fixed data for now 339 reply.setElement(3, 0x00); // fixed data for now 340 break; 341 case NceMessage.DUMMY_CMD: // Dummy instruction 342 reply.setElement(0, NCE_OKAY); // return ! CR LF 343 reply.setElement(1, 0x0D); 344 reply.setElement(2, 0x0A); 345 break; 346 case NceMessage.READ16_CMD: // Read 16 bytes 347 readMemory(m, reply, 16); 348 break; 349 case NceMessage.READ_AUI2_CMD: // Read AUI 2 byte response 350 reply.setElement(0, 0x00); // fixed data for now 351 reply.setElement(1, 0x00); // fixed data for now 352 break; 353 case NceMessage.READ1_CMD: // Read 1 bytes 354 readMemory(m, reply, 1); 355 break; 356 case NceMessage.WRITE1_CMD: // Write 1 bytes 357 writeMemory(m, reply, 1, false); 358 break; 359 case NceMessage.WRITE2_CMD: // Write 2 bytes 360 writeMemory(m, reply, 2, false); 361 break; 362 case NceMessage.WRITE4_CMD: // Write 4 bytes 363 writeMemory(m, reply, 4, false); 364 break; 365 case NceMessage.WRITE8_CMD: // Write 8 bytes 366 writeMemory(m, reply, 8, false); 367 break; 368 case NceMessage.WRITE_N_CMD: // Write n bytes 369 writeMemory(m, reply, m.getElement(3), true); 370 break; 371 case NceMessage.SEND_ACC_SIG_MACRO_CMD: // accessory command 372 accessoryCommand(m, reply); 373 break; 374 case NceMessage.READ_DIR_CV_CMD: 375 case NceMessage.READ_PAGED_CV_CMD: 376 case NceMessage.READ_REG_CMD: 377 reply.setElement(0, 123); // dummy data 378 reply.setElement(1, NCE_OKAY); // forces succeed 379 // Sample code to modify simulator response for testing purposes. 380 // Uncomment and modify as desired. 381// int cvnum = (m.getElement(1) << 8) | (m.getElement(2)); 382// if (cvnum == 7) { 383// reply.setElement(0, 88); // forces fail 384// } 385// if (cvnum == 8) { 386// reply.setElement(0, 48); // forces Hornby 387// } 388// if (cvnum == 159) { 389// reply.setElement(0, 145); 390// reply.setElement(1, NCE_DATA_OUT_OF_RANGE); // forces fail 391// } 392 break; 393 default: 394 reply.setElement(0, NCE_OKAY); // Nce okay reply! 395 } 396 return reply; 397 } 398 399 /** 400 * Write reply to output. 401 * 402 * @param r reply on message 403 */ 404 private void writeReply(NceReply r) { 405 if (r == null) { 406 return; 407 } 408 for (int i = 0; i < r.getNumDataElements(); i++) { 409 try { 410 outpipe.writeByte((byte) r.getElement(i)); 411 } catch (java.io.IOException ex) { 412 } 413 } 414 try { 415 outpipe.flush(); 416 } catch (java.io.IOException ex) { 417 } 418 } 419 420 private final byte[] turnoutMemory = new byte[256]; 421 private final byte[] macroMemory = new byte[256 * 20 + 16]; // and a little padding 422 private final byte[] consistMemory = new byte[256 * 6 + 16]; // and a little padding 423 424 /* Read NCE memory. This implementation simulates reading the NCE 425 * command station memory. There are three memory blocks that are 426 * supported, turnout status, macros, and consists. The turnout status 427 * memory is 256 bytes and starts at memory address 0xEC00. The macro memory 428 * is 256*20 or 5120 bytes and starts at memory address 0xC800 (PH5 0x6000). The consist 429 * memory is 256*6 or 1536 bytes and starts at memory address 0xF500 (PH5 0x4E00). 430 * 431 */ 432 private NceReply readMemory(NceMessage m, NceReply reply, int num) { 433 if (num > 16) { 434 log.error("Nce read memory command was greater than 16"); 435 return null; 436 } 437 int nceMemoryAddress = getNceAddress(m); 438 if (nceMemoryAddress >= tc.csm.getAccyMemAddr() && nceMemoryAddress < tc.csm.getAccyMemAddr() + tc.csm.getAccyMemSize()) { 439 log.debug("Reading turnout memory: {}", Integer.toHexString(nceMemoryAddress)); 440 int offset = m.getElement(2); 441 for (int i = 0; i < num; i++) { 442 reply.setElement(i, turnoutMemory[offset + i]); 443 } 444 return reply; 445 } 446 if (nceMemoryAddress >= tc.csm.getConsistHeadAddr() && nceMemoryAddress < tc.csm.getConsistHeadAddr() + tc.csm.getConsistMidSize()) { 447 log.debug("Reading consist memory: {}", Integer.toHexString(nceMemoryAddress)); 448 int offset = nceMemoryAddress - tc.csm.getConsistHeadAddr(); 449 450 for (int i = 0; i < num; i++) { 451 reply.setElement(i, consistMemory[offset + i]); 452 } 453 return reply; 454 } 455 if (nceMemoryAddress >= tc.csm.getMacroAddr() && nceMemoryAddress < tc.csm.getMacroAddr() + tc.csm.getMacroSize()) { 456 log.debug("Reading macro memory: {}", Integer.toHexString(nceMemoryAddress)); 457 int offset = nceMemoryAddress - tc.csm.getMacroAddr(); 458 log.debug("offset: {}", offset); 459 for (int i = 0; i < num; i++) { 460 reply.setElement(i, macroMemory[offset + i]); 461 } 462 return reply; 463 } 464 for (int i = 0; i < num; i++) { 465 reply.setElement(i, 0x00); // default fixed data 466 } 467 return reply; 468 } 469 470 private NceReply writeMemory(NceMessage m, NceReply reply, int num, boolean skipbyte) { 471 if (num > 16) { 472 log.error("Nce write memory command was greater than 16"); 473 return null; 474 } 475 int nceMemoryAddress = getNceAddress(m); 476 int byteDataBegins = 3; 477 if (skipbyte) { 478 byteDataBegins++; 479 } 480 if (nceMemoryAddress >= tc.csm.getMacroAddr() && nceMemoryAddress < tc.csm.getMacroAddr() + 256) { 481 log.debug("Writing turnout memory: {}", Integer.toHexString(nceMemoryAddress)); 482 int offset = m.getElement(2); 483 for (int i = 0; i < num; i++) { 484 turnoutMemory[offset + i] = (byte) m.getElement(i + byteDataBegins); 485 } 486 } 487 if (nceMemoryAddress >= tc.csm.getMacroAddr() && nceMemoryAddress < tc.csm.getMacroAddr() + 256 * 6) { 488 log.debug("Writing consist memory: {}", Integer.toHexString(nceMemoryAddress)); 489 int offset = nceMemoryAddress - tc.csm.getMacroAddr(); 490 for (int i = 0; i < num; i++) { 491 consistMemory[offset + i] = (byte) m.getElement(i + byteDataBegins); 492 } 493 } 494 if (nceMemoryAddress >= tc.csm.getMacroAddr() && nceMemoryAddress < tc.csm.getMacroAddr() + 256 * 20) { 495 log.debug("Writing macro memory: {}", Integer.toHexString(nceMemoryAddress)); 496 int offset = nceMemoryAddress - tc.csm.getMacroAddr(); 497 log.debug("offset: {}", offset); 498 for (int i = 0; i < num; i++) { 499 macroMemory[offset + i] = (byte) m.getElement(i + byteDataBegins); 500 } 501 } 502 reply.setElement(0, NCE_OKAY); // Nce okay reply! 503 return reply; 504 } 505 506 /** 507 * Extract item address from a message. 508 * 509 * @param m received message 510 * @return address from the message 511 */ 512 private int getNceAddress(NceMessage m) { 513 int addr = m.getElement(1); 514 addr = addr * 256; 515 addr = addr + m.getElement(2); 516 return addr; 517 } 518 519 private NceReply accessoryCommand(NceMessage m, NceReply reply) { 520 if (m.getElement(3) == 0x03 || m.getElement(3) == 0x04) { // 0x03 = close, 0x04 = throw 521 String operation = "close"; 522 if (m.getElement(3) == 0x04) { 523 operation = "throw"; 524 } 525 int nceAccessoryAddress = getNceAddress(m); 526 log.debug("Accessory command {} NT {}", operation, nceAccessoryAddress); 527 if (nceAccessoryAddress > 2044) { 528 log.error("Turnout address greater than 2044, address: {}", nceAccessoryAddress); 529 return null; 530 } 531 int bit = (nceAccessoryAddress - 1) & 0x07; 532 int setMask = 0x01; 533 for (int i = 0; i < bit; i++) { 534 setMask = setMask << 1; 535 } 536 int clearMask = 0x0FFF - setMask; 537 // log.debug("setMask: {} clearMask: {}", Integer.toHexString(setMask), Integer.toHexString(clearMask)); 538 int offset = (nceAccessoryAddress - 1) >> 3; 539 int read = turnoutMemory[offset]; 540 byte write = (byte) (read & clearMask & 0xFF); 541 542 if (operation.equals("close")) { 543 write = (byte) (write + setMask); // set bit if closed 544 } 545 turnoutMemory[offset] = write; 546 // log.debug("wrote: {}", Integer.toHexString(write)); 547 } 548 reply.setElement(0, NCE_OKAY); // Nce okay reply! 549 return reply; 550 } 551 552 private final static Logger log = LoggerFactory.getLogger(SimulatorAdapter.class); 553 554}