001package jmri.jmrix.oaktree.simulator; 002 003import java.io.*; 004 005import org.slf4j.Logger; 006import org.slf4j.LoggerFactory; 007 008// no special xSimulatorController 009import jmri.jmrix.oaktree.*; 010import jmri.util.ImmediatePipedOutputStream; 011 012/** 013 * Provide access to a simulated OakTree system. 014 * <p> 015 * Currently, the OakTree 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.oaktree.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 OakTreeSystemConnectionMemo("O", Bundle.getMessage("OakTreeSimulatorName"))); // pass customized user name 044 setManufacturer(jmri.jmrix.oaktree.SerialConnectionTypeList.OAK); 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 an OakTree 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 ((OakTreeSystemConnectionMemo) getSystemConnectionMemo()).getTrafficController().connectPort(this); 106 // do the common manager config 107 ((OakTreeSystemConnectionMemo) getSystemConnectionMemo()).configureManagers(); 108 109 // start the simulator 110 sourceThread = new Thread(this); 111 sourceThread.setName("OakTree 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 OakTree 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 OakTree command station simulation. 194 log.info("OakTree 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("OakTree 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("OakTree 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 /** 252 * This is the heart of the simulation. It translates an 253 * incoming SerialMessage into an outgoing SerialReply. 254 * See {@link jmri.jmrix.oaktree.SerialNode#markChanges(SerialReply)} and 255 * the (draft) OakTree <a href="../package-summary.html">Binary Message Format Summary</a>. 256 * 257 * @param msg the message received in the simulated node 258 * @return a single AokTree message to confirm the requested operation, or a series 259 * of messages for each (fictitious) node/pin/state. To ignore certain commands, return null. 260 */ 261 private SerialReply generateReply(SerialMessage msg) { 262 int nodeaddr = msg.getAddr(); 263 log.debug("Generate Reply to message for node {} (string = {})", nodeaddr, msg.toString()); 264 SerialReply reply = new SerialReply(); // reply length is determined by highest byte added 265 switch (msg.getElement(1)) { 266 case 48: // OakTree poll message 267 reply.setElement(0, nodeaddr); 268 reply.setElement(1, 0x50); 269 if (((OakTreeSystemConnectionMemo) getSystemConnectionMemo()).getTrafficController().getNode(nodeaddr) == null) { 270 log.debug("OakTree Sim generateReply getNode({}) = null", nodeaddr); 271 } else { 272 if (((OakTreeSystemConnectionMemo) getSystemConnectionMemo()).getTrafficController().getNode(nodeaddr).getSensorsActive()) { // input (sensors) status reply 273 log.debug("OakTree Sim generateReply for node {}", nodeaddr); 274 int payload = 0b0001; // dummy stand in for sensor status report; should we fetch known state from jmri node? 275 for (int j = 1; j < 3; j++) { 276 payload |= j << 4; 277 reply.setElement(j + 1, payload); // there could be > 5 elements TODO see SerialNode#markChanges 278 } 279 } else { 280 return null; // prevent NPE 281 } 282 } 283 log.debug("Status Reply generated {}", reply.toString()); 284 return reply; 285 default: 286 log.debug("Message ignored"); 287 return null; 288 } 289 } 290 291 /** 292 * Write reply to output. 293 * <p> 294 * Adapted from jmri.jmrix.nce.simulator.SimulatorAdapter. 295 * 296 * @param r reply on message 297 */ 298 private void writeReply(SerialReply r) { 299 if (r == null) { 300 return; // there is no reply to be sent 301 } 302 for (int i = 0; i < r.getNumDataElements(); i++) { 303 try { 304 outpipe.writeByte((byte) r.getElement(i)); 305 } catch (java.io.IOException ex) { 306 } 307 } 308 try { 309 outpipe.flush(); 310 } catch (java.io.IOException ex) { 311 } 312 } 313 314 /** 315 * Get characters from the input source. 316 * Length is always 5 bytes. 317 * <p> 318 * Only used in the Receive thread. 319 * 320 * @return filled message, only when the message is complete. 321 * @throws IOException when presented by the input source. 322 */ 323 private SerialMessage loadChars() throws java.io.IOException { 324 int i = 1; 325 int char0; 326 byte nextByte; 327 SerialMessage msg = new SerialMessage(5); 328 329 // get 1st byte 330 try { 331 byte byte0 = readByteProtected(inpipe); 332 char0 = (byte0 & 0xFF); 333 log.debug("loadChars read {}", char0); 334 msg.setElement(0, char0); // address 335 } catch (java.io.IOException e) { 336 log.debug("loadChars aborted while reading char 0"); 337 return null; 338 } 339 if (char0 > 0xFF) { 340 // skip as not a node address 341 log.debug("bit not valid as node address"); 342 } 343 344 // read in remaining packets 345 for (i = 1; i < 4; i++) { // read next 4 bytes 346 log.debug("reading rest of message in simulator, element {}", i); 347 try { 348 nextByte = readByteProtected(inpipe); 349 msg.setElement(i, nextByte); 350 } catch (java.io.IOException e) { 351 log.debug("loadChars aborted after {} chars", i); 352 break; 353 } 354 log.debug("loadChars read {} (item {})", Integer.toHexString(nextByte & 0xFF), i); 355 } 356 357 log.debug("OakTree message received by simulator"); 358 return msg; 359 } 360 361 /** 362 * Read a single byte, protecting against various timeouts, etc. 363 * <p> 364 * When a port is set to have a receive timeout (via the 365 * enableReceiveTimeout() method), some will return zero bytes or an 366 * EOFException at the end of the timeout. In that case, the read should be 367 * repeated to get the next real character. 368 * <p> 369 * Copied from DCCppSimulatorAdapter, byte[] from XNetSimAdapter 370 */ 371 private byte readByteProtected(DataInputStream istream) throws java.io.IOException { 372 byte[] rcvBuffer = new byte[1]; 373 while (true) { // loop will repeat until character found 374 int nchars; 375 nchars = istream.read(rcvBuffer, 0, 1); 376 if (nchars > 0) { 377 return rcvBuffer[0]; 378 } 379 } 380 } 381 382 // streams to share with user class 383 private DataOutputStream pout = null; // this is provided to classes who want to write to us 384 private DataInputStream pin = null; // this is provided to classes who want data from us 385 // internal ends of the pipes 386 private DataOutputStream outpipe = null; // feed pin 387 private DataInputStream inpipe = null; // feed pout 388 389 private final static Logger log = LoggerFactory.getLogger(SimulatorAdapter.class); 390 391}