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