001package jmri.jmrix.grapevine.simulator; 002 003import java.io.*; 004 005import javax.annotation.Nonnull; 006 007// no special xSimulatorController 008import jmri.jmrix.grapevine.*; 009import jmri.util.ImmediatePipedOutputStream; 010import jmri.util.swing.JmriJOptionPane; 011 012/** 013 * Provide access to a simulated Grapevine system. 014 * <p> 015 * Currently, the Grapevine SimulatorAdapter reacts to the following commands sent from the user 016 * interface with an appropriate reply {@link #generateReply(SerialMessage)}: 017 * <ul> 018 * <li>Software version (poll) 019 * <li>Renumber (displays dialog: not supported) 020 * <li>Node Init (2 replies + user configurable node-bank-bit status) 021 * <li>Set signal/sensor/turnout (echoes message) 022 * </ul> 023 * 024 * Based on jmri.jmrix.lenz.xnetsimulator.XNetSimulatorAdapter / EasyDCCSimulatorAdapter 2017 025 * <p> 026 * NOTE: Some material in this file was modified from other portions of the 027 * support infrastructure. 028 * 029 * @author Paul Bender, Copyright (C) 2009-2010 030 * @author Mark Underwood, Copyright (C) 2015 031 * @author Egbert Broerse, Copyright (C) 2018 032 */ 033@SuppressWarnings("javadoc") 034public class SimulatorAdapter extends SerialPortController implements Runnable { 035 036 // private control members 037 private Thread sourceThread; 038 039 private boolean outputBufferEmpty = true; 040 private boolean checkBuffer = true; 041 /** 042 * Simulator auto-init setting for number of banks to auto-reply on poll 043 */ 044 private int autoInit = 0; 045 046 /** 047 * Create a new SimulatorAdapter. 048 */ 049 public SimulatorAdapter() { 050 super(new GrapevineSystemConnectionMemo("G", Bundle.getMessage("GrapevineSimulatorName"))); // pass customized user name 051 option1Name = "InitPreference"; // NOI18N 052 // init pref setting, the default is No init 053 options.put(option1Name, new Option(Bundle.getMessage("AutoInitLabel"), 054 new String[]{Bundle.getMessage("ButtonNoInit"), 055 Bundle.getMessage("ButtonAll"), Bundle.getMessage("Button4Each")})); 056 setManufacturer(jmri.jmrix.grapevine.SerialConnectionTypeList.PROTRAK); 057 } 058 059 /** 060 * {@inheritDoc} 061 * Simulated input/output pipes. 062 */ 063 @Override 064 public String openPort(String portName, String appName) { 065 try { 066 PipedOutputStream tempPipeI = new ImmediatePipedOutputStream(); 067 log.debug("tempPipeI created"); 068 pout = new DataOutputStream(tempPipeI); 069 inpipe = new DataInputStream(new PipedInputStream(tempPipeI)); 070 PipedOutputStream tempPipeO = new ImmediatePipedOutputStream(); 071 outpipe = new DataOutputStream(tempPipeO); 072 pin = new DataInputStream(new PipedInputStream(tempPipeO)); 073 } catch (java.io.IOException e) { 074 log.error("init (pipe): Exception: {}", e.toString()); 075 } 076 opened = true; 077 return null; // indicates OK return 078 } 079 080 /** 081 * Set if the output buffer is empty or full. This should only be set to 082 * false by external processes. 083 * 084 * @param s true if output buffer is empty; false otherwise 085 */ 086 synchronized public void setOutputBufferEmpty(boolean s) { 087 outputBufferEmpty = s; 088 } 089 090 /** 091 * Can the port accept additional characters? The state of CTS determines 092 * this, as there seems to be no way to check the number of queued bytes and 093 * buffer length. This might go false for short intervals, but it might also 094 * stick off if something goes wrong. 095 * 096 * @return true if port can accept additional characters; false otherwise 097 */ 098 public boolean okToSend() { 099 if (checkBuffer) { 100 log.debug("Buffer Empty: {}", outputBufferEmpty); 101 return (outputBufferEmpty); 102 } else { 103 log.debug("No Flow Control or Buffer Check"); 104 return (true); 105 } 106 } 107 108 /** 109 * Set up all of the other objects to operate with a GrapevineSimulator 110 * connected to this port. 111 */ 112 @Override 113 public void configure() { 114 // connect to the traffic controller 115 log.debug("set tc for memo {}", getSystemConnectionMemo().getUserName()); 116 SerialTrafficController control = new SerialTrafficController(getSystemConnectionMemo()); 117 //compare with: XNetTrafficController packets = new XNetPacketizer(new LenzCommandStation()); 118 control.connectPort(this); 119 getSystemConnectionMemo().setTrafficController(control); 120 // do the common manager config 121 getSystemConnectionMemo().configureManagers(); 122 123 if (getOptionState(option1Name).equals(getOptionChoices(option1Name)[1])) { 124 autoInit = 1; // auto-init all bits 125 } else if (getOptionState(option1Name).equals(getOptionChoices(option1Name)[2])) { 126 autoInit = 2; // first 4 items 127 } // default = none, also after locale change just to be safe 128 129 // start the simulator 130 sourceThread = new Thread(this); 131 sourceThread.setName("Grapevine Simulator"); 132 sourceThread.setPriority(Thread.MIN_PRIORITY); 133 sourceThread.start(); 134 } 135 136 /** 137 * {@inheritDoc} 138 */ 139 @Override 140 public void connect() throws java.io.IOException { 141 log.debug("connect called"); 142 super.connect(); 143 } 144 145 // Base class methods for the Grapevine SerialPortController simulated interface 146 147 /** 148 * {@inheritDoc} 149 */ 150 @Override 151 public DataInputStream getInputStream() { 152 if (!opened || pin == null) { 153 log.error("getInputStream called before load(), stream not available"); 154 } 155 log.debug("DataInputStream pin returned"); 156 return pin; 157 } 158 159 /** 160 * {@inheritDoc} 161 */ 162 @Override 163 public DataOutputStream getOutputStream() { 164 if (!opened || pout == null) { 165 log.error("getOutputStream called before load(), stream not available"); 166 } 167 log.debug("DataOutputStream pout returned"); 168 return pout; 169 } 170 171 /** 172 * {@inheritDoc} 173 * @return always true, given this SimulatorAdapter is running 174 */ 175 @Override 176 public boolean status() { 177 return opened; 178 } 179 180 /** 181 * {@inheritDoc} 182 * 183 * @return null 184 */ 185 @Override 186 public String[] validBaudRates() { 187 log.debug("validBaudRates should not have been invoked"); 188 return new String[]{}; 189 } 190 191 /** 192 * {@inheritDoc} 193 */ 194 @Override 195 public int[] validBaudNumbers() { 196 return new int[]{}; 197 } 198 199 @Override 200 public String getCurrentBaudRate() { 201 return ""; 202 } 203 204 @Override 205 public String getCurrentPortName(){ 206 return ""; 207 } 208 209 @Override 210 public void run() { // start a new thread 211 // This thread has one task. It repeatedly reads from the input pipe 212 // and writes an appropriate response to the output pipe. This is the heart 213 // of the Grapevine command station simulation. 214 log.info("Grapevine Simulator Started"); 215 while (true) { 216 try { 217 synchronized (this) { 218 wait(50); 219 } 220 } catch (InterruptedException e) { 221 log.debug("interrupted, ending"); 222 return; 223 } 224 SerialMessage m = readMessage(); 225 SerialReply r; 226 if (log.isDebugEnabled()) { 227 StringBuffer buf = new StringBuffer(); 228 if (m != null) { 229 for (int i = 0; i < m.getNumDataElements(); i++) { 230 buf.append(Integer.toHexString(0xFF & m.getElement(i))).append(" "); 231 } 232 } else { 233 buf.append("null message buffer"); 234 } 235 log.trace("Grapevine Simulator Thread received message: {}", buf); // generates a lot of traffic 236 } 237 if (m != null) { 238 r = generateReply(m); 239 if (r != null) { // ignore errors 240 writeReply(r); 241 if (log.isDebugEnabled()) { 242 StringBuilder buf = new StringBuilder(); 243 for (int i = 0; i < r.getNumDataElements(); i++) { 244 buf.append(Integer.toHexString(0xFF & r.getElement(i))).append(" "); 245 } 246 log.debug("Grapevine Simulator Thread sent reply: {}", buf ); 247 } 248 } 249 } 250 } 251 } 252 253 /** 254 * Read one incoming message from the buffer 255 * and set outputBufferEmpty to true. 256 */ 257 private SerialMessage readMessage() { 258 SerialMessage msg = null; 259 // log.debug("Simulator reading message"); 260 try { 261 if (inpipe != null && inpipe.available() > 0) { 262 msg = loadChars(); 263 } 264 } catch (java.io.IOException e) { 265 // should do something meaningful here. 266 } 267 setOutputBufferEmpty(true); 268 return (msg); 269 } 270 271 /** 272 * This is the heart of the simulation. It translates an 273 * incoming SerialMessage into an outgoing SerialReply. 274 * See {@link jmri.jmrix.grapevine.SerialMessage}#generateReply(SerialMessage) and 275 * the Grapevine <a href="../package-summary.html">Binary Message Format Summary</a>. 276 * 277 * @param msg the message received in the simulated node 278 * @return a single Grapevine message to confirm the requested operation, or a series 279 * of messages for each (fictitious) node/pin/state. To ignore certain commands, return null. 280 */ 281 private SerialReply generateReply(SerialMessage msg) { 282 log.debug("Generate Reply to message from node {} (string = {})", msg.getAddr(), msg.toString()); 283 284 SerialReply reply = new SerialReply(); // reply length is determined by highest byte added 285 int nodeaddr = msg.getAddr(); // node addres from element(0) 286 int b1 = msg.getElement(0); // raw hex value from element(0) 287 int b2 = msg.getElement(1); // bit + state 288 int b3 = msg.getElement(2); // element(2), must repeat node address 289 int b4 = msg.getElement(3); // bank + parity 290 int bank = (b4 & 0xF0) >> 4; // bank # on node, 0 on node initialization 291 log.debug("Message nodeaddress={} b1={} b2={} b3={} b4={}", nodeaddr, b1, b2, b3, b4); 292 293 if (nodeaddr == 0) { // error 294 log.debug("general error: coded as: {}", (((b4 & 0x70) << 4) - 1)); 295 return null; 296 } 297 298 switch (b2) { 299 300 case 119: 301 log.debug("get software version (poll) message detected"); 302 // 2 byte software version number reply 303 reply.setElement(0, nodeaddr | 0x80); 304 reply.setElement(1, 9); // pretend version "9" 305 // no parity 306 break; 307 308 case 0x71 : 309 log.debug("init node message 1 detected - ASD sensors"); 310 // init reply as set in prefs autoInit 311 if (autoInit > 0) { // not disabled 312 log.debug("start init 1 of node {}", nodeaddr); 313 nodeResponse(nodeaddr, 1, 1, autoInit); // banks 1-4 314 } 315 // all replies are generated and sent by nodeResponse() 316 reply = null; 317 break; 318 319 case 0x73: //(b2 == 0x70) && ((b4 & 0xF0) == 0x10) 320 log.debug("init node message 2 detected - parallel sensors"); 321 // init reply as set in prefs autoInit 322 if (autoInit > 0) { // not disabled 323 log.debug("start init 2 of node {}", nodeaddr); 324 nodeResponse(nodeaddr, 5, 5, autoInit); // bank 5 = parallel 325 } 326 // all replies are generated and sent by nodeResponse() 327 reply = null; 328 break; 329 330 default: 331 if (bank == 0x6) { // this is the rename command, with element 2 = new node number 332 JmriJOptionPane.showMessageDialog(null, 333 Bundle.getMessage("RenumberSupport"), 334 Bundle.getMessage("MessageTitle"), 335 JmriJOptionPane.ERROR_MESSAGE); 336 log.debug("rename command not supported, old address: {}, new address: {}, bank: {}", 337 nodeaddr, b2, bank); 338 } else { 339 log.debug("echo normal command, node {} bank {} ignored", nodeaddr, bank); 340 reply = null; // ignore all other messages 341 // alternatavely, send a 4 byte general reply: 342 // reply.setElement(0, (nodeaddr | 0x80)); 343 // reply.setElement(1, (b2 & 0xFF)); // normally: bit + state 344 // reply.setElement(2, (nodeaddr | 0x80)); 345 // reply.setElement(3, (bank << 4)); // 0 = error, bank 1..3 for signals, 4..5 sensors (and parity) 346 // reply = setParity(reply, 0); 347 } 348 } 349 log.debug("Reply {}", reply == null ? "empty, Message ignored" : " generated " + reply.toString()); 350 return reply; 351 } 352 353 /** 354 * Write reply to output. 355 * <p> 356 * Adapted from jmri.jmrix.nce.simulator.SimulatorAdapter. 357 * 358 * @param r reply on message 359 */ 360 private void writeReply(@Nonnull SerialReply r) { 361 for (int i = 0; i < r.getNumDataElements(); i++) { 362 try { 363 outpipe.writeByte((byte) r.getElement(i)); 364 } catch (java.io.IOException ignored) { 365 } 366 } 367 try { 368 outpipe.flush(); 369 } catch (java.io.IOException ignored) { 370 } 371 } 372 373 /** 374 * Get characters from the input source. 375 * <p> 376 * Only used in the Receive thread. 377 * 378 * @return filled message, only when the message is complete. 379 * @throws IOException when presented by the input source. 380 */ 381 private SerialMessage loadChars() throws java.io.IOException { 382 int nchars; 383 byte[] rcvBuffer = new byte[32]; 384 385 nchars = inpipe.read(rcvBuffer, 0, 32); 386 //log.debug("new message received"); 387 SerialMessage msg = new SerialMessage(nchars); 388 389 for (int i = 0; i < nchars; i++) { 390 msg.setElement(i, rcvBuffer[i] & 0xFF); 391 } 392 return msg; 393 } 394 395 /** 396 * Set parity on simulated Grapevine Node reply. 397 * Code copied from {@link SerialMessage#setParity(int)} 398 * 399 * @param r the SerialReply to complete 400 * @param start bit index to start 401 * @return SerialReply with parity set 402 */ 403 public SerialReply setParity(SerialReply r, int start) { 404 // nibble sum method 405 int sum = r.getElement(0 + start) & 0x0F; 406 sum += (r.getElement(0 + start) & 0x70) >> 4; 407 sum += (r.getElement(1 + start) * 2) & 0x0F; 408 sum += ((r.getElement(1 + start) * 2) & 0xF0) >> 4; 409 sum += (r.getElement(3 + start) & 0x70) >> 4; 410 //log.debug("Parity element read: {}", 411 // Integer.toHexString(r.getElement(3 + start) & 0x70)); 412 int parity = 16 - (sum & 0xF); 413 414 r.setElement(3 + start, (r.getElement(3 + start) & 0xF0) | (parity & 0xF)); 415 return r; 416 } 417 418 int signalBankSize = 16; // theoretically: 16 419 int sensorBankSize = 64; // theoretically: 0x3F 420 javax.swing.Timer timer; 421 422 /** 423 * Pretend a node init reply for a range of banks and bits. Is this a proper simulation of hardware? 424 * <p> 425 * Based on information in jmri.jmrix.grapevine.SerialMessage#staticFormat(int, int, int, int). 426 * 427 * @param node the node address 428 * @param startBank first bank id to report 429 * @param endBank last bank id to report 430 * @param initBits number of inputs/output bits to report 431 */ 432 private void nodeResponse(int node, int startBank, int endBank, int initBits) { 433 if (node < 1 || node > 127) { // node address invalid 434 log.warn("Invalid Node Address; no response generated"); 435 return; 436 } 437 if (initBits > 1) { // leave at max when 1 438 signalBankSize = 4; // only first 4 signal bits reporting 439 sensorBankSize = 4; // only first 4 sensor bits reporting 440 } 441 int b1 = -1; 442 int b2 = -1; 443 int b3 = -1; 444 int b4 = -1; 445 446 SerialReply nReply = new SerialReply(); // reply length is determined by highest byte added 447 nReply.setElement(0, node | 0x80); 448 nReply.setElement(2, node | 0x80); 449 450 for (int k = startBank; k <= endBank; k++) { // bank 451 if (k <= 3) { // bank 1 to 3, signals 452 nReply.setElement(3, (k << 4)); // bank (bit 1234): 1-3 = signals 453 log.debug("element 3 set to 0x{} - {}", (k << 4) & 0x70, Integer.toBinaryString((k << 4) & 0x70)); 454 455 for (int j = 1; j < signalBankSize; j++) { // bits, send state of each signal bit (banks 1, 2, 3) 456 log.debug("Sending signal state of node {}, bank {}, bit {}", node, k, j); 457 nReply.setElement(1, ((j << 3) | 0x6) & 0x7F); // bit id (bits 2345) + state (bits 678): set to Red 458 459 nReply = setParity(nReply, 0); 460 writeReply(nReply); 461 // check 462 b1 = nReply.getElement(0) & 0x7F; // raw hex value from element(0) 463 b2 = nReply.getElement(1) & 0x7F; // bit + state 464 b3 = nReply.getElement(2) & 0x7F; // element(2), repeat node address 465 b4 = nReply.getElement(3) & 0xFF; // bank + parity 466 if (b1 != b3) { 467 log.error("Address mismatch on node {} bank {} bit {}", node, k, j); 468 } 469 log.debug("Reply written for node {} bank {} bit {}: b1= {} b2={} b3={} b4={}", node, k, j, b1, b2, b3, b4); 470 log.debug("Reply as hex: {} {} {} {}", Integer.toHexString(b1), 471 Integer.toHexString(b2), Integer.toHexString(b3), Integer.toHexString(b4)); 472 log.debug("Reply as bin: {} - {} - {} - {}", Integer.toBinaryString(b1), 473 Integer.toBinaryString(b2), Integer.toBinaryString(b3), Integer.toBinaryString(b4)); 474 } 475 } else { // bank 4 and 5, sensors 476 nReply.setElement(3, (k << 4)); // bank (bit 1234): 4-5 = sensors 477 log.debug("element 3 set to 0x{} - {}", (k << 4) & 0x70, Integer.toBinaryString((k << 4) & 0x70)); 478 479 for (int j = 1; j < sensorBankSize; j++) { // bits, send state of each sensor bit (banks 4, 5) 480 log.debug("Sending sensor state of node {}, bank {}, bit {}", node, k, j); 481 nReply.setElement(1, ((j << 1) | 0x1) & 0x7F); // bit id (bits 234567) + state (bit 8): inactive 482 483 nReply = setParity(nReply,0); 484 writeReply(nReply); 485 // check 486 b1 = nReply.getElement(0) & 0x7F; // raw hex value from element(0) 487 b2 = nReply.getElement(1) & 0x7F; // bit + state 488 b3 = nReply.getElement(2) & 0x7F; // element(2), repeat node address 489 b4 = nReply.getElement(3) & 0xFF; // bank + parity 490 if (b1 != b3) { 491 log.error("Address mismatch on node {} bank {} bit {}", node, k, j); 492 } 493 log.debug("Reply written for node {} bank {} bit {}: b1= {} b2={} b3={} b4={}", node, k, j, b1, b2, b3, b4); 494 log.debug("Reply as hex: {} {} {} {}", Integer.toHexString(b1), 495 Integer.toHexString(b2), Integer.toHexString(b3), Integer.toHexString(b4)); 496 log.debug("Reply as bin: {} - {} - {} - {}", Integer.toBinaryString(b1), 497 Integer.toBinaryString(b2), Integer.toBinaryString(b3), Integer.toBinaryString(b4)); } 498 } 499 } 500 } 501 502 // streams to share with user class 503 private DataOutputStream pout = null; // this is provided to classes who want to write to us 504 private DataInputStream pin = null; // this is provided to classes who want data from us 505 // internal ends of the pipes 506 private DataOutputStream outpipe = null; // feed pin 507 private DataInputStream inpipe = null; // feed pout 508 509 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(SimulatorAdapter.class); 510 511}