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