001package jmri.jmrix.secsi.simulator; 002 003import java.io.DataInputStream; 004import java.io.DataOutputStream; 005import java.io.IOException; 006import java.io.PipedInputStream; 007import java.io.PipedOutputStream; 008import jmri.jmrix.secsi.SerialMessage; 009import jmri.jmrix.secsi.SerialPortController; // no special xSimulatorController 010import jmri.jmrix.secsi.SerialReply; 011import jmri.jmrix.secsi.SecsiSystemConnectionMemo; 012import jmri.util.ImmediatePipedOutputStream; 013import org.slf4j.Logger; 014import org.slf4j.LoggerFactory; 015 016/** 017 * Provide access to a simulated SECSI system. 018 * <p> 019 * Currently, the Secsi SimulatorAdapter reacts to the following commands sent from the user 020 * interface with an appropriate reply {@link #generateReply(SerialMessage)}: 021 * <ul> 022 * <li>Poll (length = 1, reply length = 2) 023 * </ul> 024 * 025 * Based on jmri.jmrix.grapevine.simulator.SimulatorAdapter 2018 026 * <p> 027 * NOTE: Some material in this file was modified from other portions of the 028 * support infrastructure. 029 * 030 * @author Paul Bender, Copyright (C) 2009-2010 031 * @author Mark Underwood, Copyright (C) 2015 032 * @author Egbert Broerse, Copyright (C) 2018 033 */ 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 /** 043 * Create a new SimulatorAdapter. 044 */ 045 public SimulatorAdapter() { 046 super(new SecsiSystemConnectionMemo("V", Bundle.getMessage("SecsiSimulatorName"))); // pass customized user name 047 setManufacturer(jmri.jmrix.secsi.SerialConnectionTypeList.TRACTRONICS); 048 } 049 050 /** 051 * {@inheritDoc} 052 * Simulated input/output pipes. 053 */ 054 @Override 055 public String openPort(String portName, String appName) { 056 try { 057 PipedOutputStream tempPipeI = new ImmediatePipedOutputStream(); 058 log.debug("tempPipeI created"); 059 pout = new DataOutputStream(tempPipeI); 060 inpipe = new DataInputStream(new PipedInputStream(tempPipeI)); 061 log.debug("inpipe created {}", inpipe != null); 062 PipedOutputStream tempPipeO = new ImmediatePipedOutputStream(); 063 outpipe = new DataOutputStream(tempPipeO); 064 pin = new DataInputStream(new PipedInputStream(tempPipeO)); 065 } catch (java.io.IOException e) { 066 log.error("init (pipe): Exception: {}", e.toString()); 067 } 068 opened = true; 069 return null; // indicates OK return 070 } 071 072 /** 073 * Set if the output buffer is empty or full. This should only be set to 074 * false by external processes. 075 * 076 * @param s true if output buffer is empty; false otherwise 077 */ 078 synchronized public void setOutputBufferEmpty(boolean s) { 079 outputBufferEmpty = s; 080 } 081 082 /** 083 * Can the port accept additional characters? The state of CTS determines 084 * this, as there seems to be no way to check the number of queued bytes and 085 * buffer length. This might go false for short intervals, but it might also 086 * stick off if something goes wrong. 087 * 088 * @return true if port can accept additional characters; false otherwise 089 */ 090 public boolean okToSend() { 091 if (checkBuffer) { 092 log.debug("Buffer Empty: {}", outputBufferEmpty); 093 return (outputBufferEmpty); 094 } else { 095 log.debug("No Flow Control or Buffer Check"); 096 return (true); 097 } 098 } 099 100 /** 101 * Set up all of the other objects to operate with a SECSI 102 * connected to this port. 103 */ 104 @Override 105 public void configure() { 106 // connect to the traffic controller 107 log.debug("set tc for memo {}", getSystemConnectionMemo().getUserName()); 108 ((SecsiSystemConnectionMemo) getSystemConnectionMemo()).getTrafficController().connectPort(this); 109 // do the common manager config 110 ((SecsiSystemConnectionMemo) getSystemConnectionMemo()).configureManagers(); 111 112 // start the simulator 113 sourceThread = new Thread(this); 114 sourceThread.setName("Secsi Simulator"); 115 sourceThread.setPriority(Thread.MIN_PRIORITY); 116 sourceThread.start(); 117 } 118 119 /** 120 * {@inheritDoc} 121 */ 122 @Override 123 public void connect() throws java.io.IOException { 124 log.debug("connect called"); 125 super.connect(); 126 } 127 128 // Base class methods for the SECSI SerialPortController simulated interface 129 130 /** 131 * {@inheritDoc} 132 */ 133 @Override 134 public DataInputStream getInputStream() { 135 if (!opened || pin == null) { 136 log.error("getInputStream called before load(), stream not available"); 137 } 138 log.debug("DataInputStream pin returned"); 139 return pin; 140 } 141 142 /** 143 * {@inheritDoc} 144 */ 145 @Override 146 public DataOutputStream getOutputStream() { 147 if (!opened || pout == null) { 148 log.error("getOutputStream called before load(), stream not available"); 149 } 150 log.debug("DataOutputStream pout returned"); 151 return pout; 152 } 153 154 /** 155 * {@inheritDoc} 156 * @return always true, given this SimulatorAdapter is running 157 */ 158 @Override 159 public boolean status() { 160 return opened; 161 } 162 163 /** 164 * {@inheritDoc} 165 * 166 * @return null 167 */ 168 @Override 169 public String[] validBaudRates() { 170 log.debug("validBaudRates should not have been invoked"); 171 return new String[]{}; 172 } 173 174 /** 175 * {@inheritDoc} 176 */ 177 @Override 178 public int[] validBaudNumbers() { 179 return new int[]{}; 180 } 181 182 @Override 183 public String getCurrentBaudRate() { 184 return ""; 185 } 186 187 @Override 188 public String getCurrentPortName(){ 189 return ""; 190 } 191 192 @Override 193 public void run() { // start a new thread 194 // This thread has one task. It repeatedly reads from the input pipe 195 // and writes an appropriate response to the output pipe. This is the heart 196 // of the Secsi command station simulation. 197 log.info("Secsi Simulator Started"); 198 while (true) { 199 try { 200 synchronized (this) { 201 wait(50); 202 } 203 } catch (InterruptedException e) { 204 log.debug("interrupted, ending"); 205 return; 206 } 207 SerialMessage m = readMessage(); 208 SerialReply r; 209 if (log.isTraceEnabled()) { 210 StringBuilder buf = new StringBuilder(); 211 if (m != null) { 212 for (int i = 0; i < m.getNumDataElements(); i++) { 213 buf.append(Integer.toHexString(0xFF & m.getElement(i))).append(" "); 214 } 215 } else { 216 buf.append("null message buffer"); 217 } 218 log.trace("Secsi Simulator Thread received message: {}", buf ); // generates a lot of traffic 219 } 220 if (m != null) { 221 r = generateReply(m); 222 if (r != null) { // ignore errors and null replies 223 writeReply(r); 224 if (log.isDebugEnabled()) { 225 StringBuilder buf = new StringBuilder(); 226 for (int i = 0; i < r.getNumDataElements(); i++) { 227 buf.append(Integer.toHexString(0xFF & r.getElement(i))).append(" "); 228 } 229 log.debug("Secsi Simulator Thread sent reply: {}", buf ); 230 } 231 } 232 } 233 } 234 } 235 236 /** 237 * Read one incoming message from the buffer 238 * and set outputBufferEmpty to true. 239 */ 240 private SerialMessage readMessage() { 241 SerialMessage msg = null; 242 // log.debug("Simulator reading message"); // lots of traffic in loop 243 try { 244 if (inpipe != null && inpipe.available() > 0) { 245 msg = loadChars(); 246 } 247 } catch (java.io.IOException e) { 248 // should do something meaningful here. 249 } 250 setOutputBufferEmpty(true); 251 return (msg); 252 } 253 254 // operational instance variable (not preserved between runs) 255 protected boolean[] nodesSet = new boolean[128]; // node init received and replied? 256 257 /** 258 * This is the heart of the simulation. It translates an 259 * incoming SerialMessage into an outgoing SerialReply. 260 * See {@link jmri.jmrix.secsi.SerialNode#markChanges(SerialReply)} and 261 * the (draft) secsi <a href="../package-summary.html">Binary Message Format Summary</a>. 262 * 263 * @param msg the message received in the simulated node 264 * @return a single Secsi message to confirm the requested operation, or a series 265 * of messages for each (fictitious) node/pin/state. To ignore certain commands, return null. 266 */ 267 private SerialReply generateReply(SerialMessage msg) { 268 int nodeaddr = msg.getAddr(); 269 log.debug("Generate Reply to message for node {} (string = {})", nodeaddr, msg.toString()); 270 SerialReply reply = new SerialReply(); // reply length is determined by highest byte added 271// if (nodesSet[nodeaddr] != true) { // only Polls expect a reply from the node 272 switch (msg.getNumDataElements()) { 273 case 1: // poll message, but reading msg received often fails (see case 9) 274 log.debug("Poll message detected by simulator"); 275 reply.setElement(0, nodeaddr); // node address from msg element(0) 276 reply.setElement(1, 0x30); // poll reply contains just 2 elements, second is 0x48 (see SerialMessage#isPoll()) 277 nodesSet[nodeaddr] = true; // mark node as inited 278 log.debug("Poll reply generated {}", reply.toString()); 279 return reply; 280 case 5: // standard secsi sensor state request message 281 if (((SecsiSystemConnectionMemo) getSystemConnectionMemo()).getTrafficController().getNode(nodeaddr).getSensorsActive()) { // input (sensors) status reply 282 int payload = 0b0101; // dummy stand in for sensor status report; should we fetch known state from jmri node? 283 for (int j = 0; j < 3; j++) { 284 payload |= j << 4; 285 reply.setElement(j + 1, payload); 286 } 287 log.debug("Status Reply generated {}", reply.toString()); 288 } 289 return reply; 290 case 9: 291 // use this message to confirm node poll? 292 //reply.setElement(0, nodeaddr); // node address from msg element(0) 293 //reply.setElement(1, 48); // poll reply contains just 2 elements, second is 0x48 (see SerialMessage#isPoll()) 294 log.debug("Outpacket received"); // Poll Reply generated: {}", reply.toString()); 295 return null; // reply; 296 default: 297 log.debug("Message (other) ignored"); 298 return null; 299 } 300 // Poll will give an error: 301 // jmrix.AbstractMRTrafficController ERROR - Transmit thread terminated prematurely by: 302 // java.lang.ArrayIndexOutOfBoundsException: 1 [secsi.SerialTrafficController Transmit thread] 303 } 304 305 /** 306 * Write reply to output. 307 * <p> 308 * Adapted from jmri.jmrix.nce.simulator.SimulatorAdapter. 309 * 310 * @param r reply on message 311 */ 312 private void writeReply(SerialReply r) { 313 if (r == null) { 314 return; // there is no reply to be sent 315 } 316 for (int i = 0; i < r.getNumDataElements(); i++) { 317 try { 318 outpipe.writeByte((byte) r.getElement(i)); 319 } catch (java.io.IOException ex) { 320 } 321 } 322 try { 323 outpipe.flush(); 324 } catch (java.io.IOException ex) { 325 } 326 } 327 328 private int[] lastChars = new int[9]; // temporary store of bytes received, excluding node address 329 private int nextNodeAddress; 330 private boolean lastCharLoaded = false; 331 332 /** 333 * Get characters from the input source. No opcode, so must read per byte. 334 * Length will be either 1, 5 or 9 bytes. 335 * <p> 336 * Only used in the Receive thread. 337 * 338 * @return filled message, only when the message is complete. 339 * @throws IOException when presented by the input source. 340 */ 341 private SerialMessage loadChars() throws java.io.IOException { 342 int i = 1; 343 int char0; 344 byte nextByte; 345 346 // get 1st byte, see if ending too soon 347 if (lastCharLoaded && (nextNodeAddress < 0x2F)) { // use char previously read fom pipe as element 0 (node address) 348 char0 = nextNodeAddress; 349 lastCharLoaded = false; 350 } else { 351 try { 352 byte byte0 = readByteProtected(inpipe); 353 char0 = (byte0 & 0xFF); 354 log.debug("loadChars read {}", char0); 355 } catch (java.io.IOException e) { 356 lastCharLoaded = false; // we lost track 357 log.debug("loadChars aborted while reading char 0"); 358 return null; 359 } 360 } 361 if (char0 > 0x2F) { 362 // skip as not a node address 363 log.debug("bit not valid as node address"); 364 } 365 366 // try if what is received is a series of outpackets 367 for (i = 1; i < 9; i++) { // reading next max 8 bytes 368 log.debug("reading rest of message in simulator, element {}", i); 369 try { 370 nextByte = readByteProtected(inpipe); 371 } catch (java.io.IOException e) { 372 log.debug("loadChars aborted after {} chars", i); 373 lastCharLoaded = false; // we lost track 374 //i = i - 1; // current message complete at previous char 375 log.debug("overshot reading Secsi message at element {}. Ready", i); 376 break; 377 } 378 log.debug("loadChars read {} (item {})", Integer.toHexString(nextByte & 0xFF), i); 379 // check if it is one of the 8 byte 0x .. 7x Outpackets series 380 if ((nextByte & 0xFF) >> 4 == i - 1) { // pattern for next element in range of increasing 0x .. 7x Outpackets 381 lastChars[i] = (nextByte & 0xFF); 382 log.debug("matched item {} in series: {}", i, (nextByte & 0xFF) >> 4); 383 } else if ((nextByte & 0xFF) < 0x2F) { // if it's not, store last item read as first element of next message 384 // nextChar could be node address again, in that case the preceding was perhaps a single node poll message 385 // but on node 00 could follow the first of the outputpacket series 00 10 etc. 386 nextNodeAddress = (nextByte & 0xFF); // store value in array 387 lastCharLoaded = true; 388 i = Math.max(1, i - 1); // current message complete at previous char 389 log.debug("overshot reading Secsi message at element {}. Next node = {}", i, nextNodeAddress); 390 break; 391 } else { // we lost this series, but previous item could have been the next new node address 392 if ((lastChars[i - 1] >= 0) && (lastChars[i - 1] < 0x2F)) { // valid as node address 393 nextNodeAddress = lastChars[i - 1]; 394 lastCharLoaded = true; // store last byte read as possible next node address 395 i = Math.max(1, i - 1); // current message complete before previous char 396 log.debug("overshot Secsi message at element {}. Next node = {}", i, nextNodeAddress); 397 break; 398 } else { // unhandled message type 399 lastCharLoaded = false; // discard last byte read as not making sense 400 i = Math.max(1, i - 1); // current message complete at previous char 401 log.debug("unhandled Secsi message from element {}", i); 402 break; 403 } 404 } 405 } 406 407 // copy bytes to Message 408 SerialMessage msg = new SerialMessage(i); 409 msg.setElement(0, char0); // address 410 for (int k = 1; k < i; k++) { // copy remaining bytes if i > 1 411 msg.setElement(k, lastChars[k]); 412 } 413 log.debug("Secsi message received by simulator, length = {}", i); 414 if (msg.getNumDataElements() == 1) { 415 nodesSet[char0] = false; // reset first node poll 416 } 417 return msg; 418 } 419 420 /** 421 * Read a single byte, protecting against various timeouts, etc. 422 * <p> 423 * When a port is set to have a receive timeout (via the 424 * enableReceiveTimeout() method), some will return zero bytes or an 425 * EOFException at the end of the timeout. In that case, the read should be 426 * repeated to get the next real character. 427 * <p> 428 * Copied from DCCppSimulatorAdapter, byte[] from XNetSimAdapter 429 */ 430 private byte readByteProtected(DataInputStream istream) throws java.io.IOException { 431 byte[] rcvBuffer = new byte[1]; 432 while (true) { // loop will repeat until character found 433 int nchars; 434 nchars = istream.read(rcvBuffer, 0, 1); 435 if (nchars > 0) { 436 return rcvBuffer[0]; 437 } 438 } 439 } 440 441 // streams to share with user class 442 private DataOutputStream pout = null; // this is provided to classes who want to write to us 443 private DataInputStream pin = null; // this is provided to classes who want data from us 444 // internal ends of the pipes 445 private DataOutputStream outpipe = null; // feed pin 446 private DataInputStream inpipe = null; // feed pout 447 448 private final static Logger log = LoggerFactory.getLogger(SimulatorAdapter.class); 449 450}