001package jmri.jmrix.maple.simulator; 002 003import java.io.*; 004 005import org.slf4j.Logger; 006import org.slf4j.LoggerFactory; 007 008// no special xSimulatorController 009import jmri.jmrix.maple.*; 010import jmri.util.ImmediatePipedOutputStream; 011 012/** 013 * Provide access to a simulated Maple system. 014 * <p> 015 * Currently, the Maple SimulatorAdapter reacts to the following commands sent from the user 016 * interface with an appropriate reply {@link #generateReply(SerialMessage)}: 017 * <ul> 018 * <li>RC Read Coils (poll), all coil bits 0 019 * <li>WC Write Coils (ACK) 020 * </ul> 021 * 022 * Based on jmri.jmrix.lenz.xnetsimulator.XNetSimulatorAdapter / GrapevineSimulatorAdapter 2017 023 * <p> 024 * NOTE: Some material in this file was modified from other portions of the 025 * support infrastructure. 026 * 027 * @author Paul Bender, Copyright (C) 2009-2010 028 * @author Mark Underwood, Copyright (C) 2015 029 * @author Egbert Broerse, Copyright (C) 2018 030 */ 031@SuppressWarnings("javadoc") 032public class SimulatorAdapter extends SerialPortController implements Runnable { 033 034 // private control members 035 private Thread sourceThread; 036 037 private boolean outputBufferEmpty = true; 038 private boolean checkBuffer = true; 039 040 /** 041 * Create a new SimulatorAdapter. 042 */ 043 public SimulatorAdapter() { 044 super(new MapleSystemConnectionMemo("K", Bundle.getMessage("MapleSimulatorName"))); // pass customized user name 045 setManufacturer(jmri.jmrix.maple.SerialConnectionTypeList.MAPLE); 046 } 047 048 /** 049 * {@inheritDoc} 050 * Simulated input/output pipes. 051 */ 052 @Override 053 public String openPort(String portName, String appName) { 054 try { 055 PipedOutputStream tempPipeI = new ImmediatePipedOutputStream(); 056 log.debug("tempPipeI created"); 057 pout = new DataOutputStream(tempPipeI); 058 inpipe = new DataInputStream(new PipedInputStream(tempPipeI)); 059 log.debug("inpipe created {}", inpipe != null); 060 PipedOutputStream tempPipeO = new ImmediatePipedOutputStream(); 061 outpipe = new DataOutputStream(tempPipeO); 062 pin = new DataInputStream(new PipedInputStream(tempPipeO)); 063 } catch (java.io.IOException e) { 064 log.error("init (pipe): Exception: {}", e.toString()); 065 } 066 opened = true; 067 return null; // indicates OK return 068 } 069 070 /** 071 * Set if the output buffer is empty or full. This should only be set to 072 * false by external processes. 073 * 074 * @param s true if output buffer is empty; false otherwise 075 */ 076 synchronized public void setOutputBufferEmpty(boolean s) { 077 outputBufferEmpty = s; 078 } 079 080 /** 081 * Can the port accept additional characters? The state of CTS determines 082 * this, as there seems to be no way to check the number of queued bytes and 083 * buffer length. This might go false for short intervals, but it might also 084 * stick off if something goes wrong. 085 * 086 * @return true if port can accept additional characters; false otherwise 087 */ 088 public boolean okToSend() { 089 if (checkBuffer) { 090 log.debug("Buffer Empty: {}", outputBufferEmpty); 091 return (outputBufferEmpty); 092 } else { 093 log.debug("No Flow Control or Buffer Check"); 094 return (true); 095 } 096 } 097 098 /** 099 * Set up all of the other objects to operate with a MapleSimulator 100 * connected to this port. 101 */ 102 @Override 103 public void configure() { 104 log.debug("set tc for memo {}", getSystemConnectionMemo().getUserName()); 105 // connect to the traffic controller 106 ((MapleSystemConnectionMemo) getSystemConnectionMemo()).getTrafficController().connectPort(this); 107 // do the common manager config 108 ((MapleSystemConnectionMemo) getSystemConnectionMemo()).configureManagers(); 109 110 // start the simulator 111 sourceThread = new Thread(this); 112 sourceThread.setName("Maple Simulator"); 113 sourceThread.setPriority(Thread.MIN_PRIORITY); 114 sourceThread.start(); 115 } 116 117 /** 118 * {@inheritDoc} 119 */ 120 @Override 121 public void connect() throws java.io.IOException { 122 log.debug("connect called"); 123 super.connect(); 124 } 125 126 // Base class methods for the Maple SerialPortController simulated interface 127 128 /** 129 * {@inheritDoc} 130 */ 131 @Override 132 public DataInputStream getInputStream() { 133 if (!opened || pin == null) { 134 log.error("getInputStream called before load(), stream not available"); 135 } 136 log.debug("DataInputStream pin returned"); 137 return pin; 138 } 139 140 /** 141 * {@inheritDoc} 142 */ 143 @Override 144 public DataOutputStream getOutputStream() { 145 if (!opened || pout == null) { 146 log.error("getOutputStream called before load(), stream not available"); 147 } 148 log.debug("DataOutputStream pout returned"); 149 return pout; 150 } 151 152 /** 153 * {@inheritDoc} 154 * @return always true, given this SimulatorAdapter is running 155 */ 156 @Override 157 public boolean status() { 158 return opened; 159 } 160 161 /** 162 * {@inheritDoc} 163 * 164 * @return null 165 */ 166 @Override 167 public String[] validBaudRates() { 168 log.debug("validBaudRates should not have been invoked"); 169 return new String[]{}; 170 } 171 172 /** 173 * {@inheritDoc} 174 */ 175 @Override 176 public int[] validBaudNumbers() { 177 return new int[]{}; 178 } 179 180 @Override 181 public String getCurrentBaudRate() { 182 return ""; 183 } 184 185 @Override 186 public String getCurrentPortName(){ 187 return ""; 188 } 189 190 @Override 191 public void run() { // start a new thread 192 // This thread has one task. It repeatedly reads from the input pipe 193 // and writes an appropriate response to the output pipe. This is the heart 194 // of the Maple command station simulation. 195 log.info("Maple Simulator Started"); 196 while (true) { 197 try { 198 synchronized (this) { 199 wait(50); 200 } 201 } catch (InterruptedException e) { 202 log.debug("interrupted, ending"); 203 return; 204 } 205 SerialMessage m = readMessage(); 206 SerialReply r; 207 if (log.isTraceEnabled()) { 208 StringBuilder buf = new StringBuilder(); 209 if (m != null) { 210 for (int i = 0; i < m.getNumDataElements(); i++) { 211 buf.append(Integer.toHexString(0xFF & m.getElement(i))).append(" "); 212 } 213 } else { 214 buf.append("null message buffer"); 215 } 216 log.trace("Maple Simulator Thread received message: {}", buf); // generates a lot of traffic 217 } 218 if (m != null) { 219 r = generateReply(m); 220 if (r != null) { // ignore errors 221 writeReply(r); 222 if (log.isDebugEnabled()) { 223 StringBuilder buf = new StringBuilder(); 224 for (int i = 0; i < r.getNumDataElements(); i++) { 225 buf.append(Integer.toHexString(0xFF & r.getElement(i))).append(" "); 226 } 227 log.debug("Maple Simulator Thread sent reply: {}", buf); 228 } 229 } 230 } 231 } 232 } 233 234 /** 235 * Read one incoming message from the buffer 236 * and set outputBufferEmpty to true. 237 */ 238 private SerialMessage readMessage() { 239 SerialMessage msg = null; 240 // log.debug("Simulator reading message"); 241 try { 242 if (inpipe != null && inpipe.available() > 0) { 243 msg = loadChars(); 244 } 245 } catch (java.io.IOException e) { 246 // should do something meaningful here. 247 } 248 setOutputBufferEmpty(true); 249 return (msg); 250 } 251 252 /** 253 * This is the heart of the simulation. It translates an 254 * incoming SerialMessage into an outgoing SerialReply. 255 * See {@link jmri.jmrix.maple.SerialMessage}. 256 * 257 * @param msg the message received in the simulated node 258 * @return a single Maple 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 log.debug("Generate Reply to message from node {} (string = {})", msg.getAddress(), msg.toString()); 263 264 SerialReply reply = new SerialReply(); // reply length is determined by highest byte added 265 int nodeAddress = msg.getUA(); // node addres from element 1 + 2 266 //convert hex to character 267 char cmd1 = (char) msg.getElement(3); // command char 1 268 char cmd2 = (char) msg.getElement(4); // command char 2 269 270 log.debug("Message nodeaddress={} cmd={}{}, Start={}, Num={}", 271 nodeAddress, cmd1, cmd2, 272 getStartAddress(msg), getNumberOfCoils(msg)); 273 274 switch ("" + cmd1 + cmd2) { 275 case "RC": // Read Coils message 276 log.debug("Read Coils (poll) message detected"); 277 int i = 1; 278 // init reply 279 log.debug("RC Reply from node {}", nodeAddress); 280 reply.setElement(0, 0x02); // <STX> 281 reply.setElement(1, msg.getElement(1)); 282 reply.setElement(2, msg.getElement(2)); 283 reply.setElement(3, 'R'); 284 reply.setElement(4, 'C'); 285 for (i = 1; i < getNumberOfCoils(msg); i++) { 286 reply.setElement(i + 4, 0x00); // report state of each requested coil as Inactive = 0 287 // TODO: echo commanded state from JMRI node-bit using: getCommandedState(nodeAddress * 1000 + getStartAddress(msg) + 1) 288 } 289 reply.setElement(i + 5, 0x03); 290 reply = setChecksum(reply, i + 6); 291 break; 292 case "WC": // Write Coils message 293 log.debug("Write Coils message detected"); 294 // init reply 295 log.debug("WC Reply from node {}", nodeAddress); 296 reply.setElement(0, 0x06); // <ACK> 297 reply.setElement(1, msg.getElement(1)); 298 reply.setElement(2, msg.getElement(2)); 299 reply.setElement(3, 'W'); 300 reply.setElement(4, 'C'); 301 break; 302 default: 303 // TODO "WC" message replies 304 log.debug("command ignored"); 305 reply = null; // ignore all other messages 306 } 307 log.debug("Reply {}", reply == null ? "empty, Message ignored" : "generated " + reply.toString()); 308 return (reply); 309 } 310 311 /** 312 * Extract start coils from RC/WC message. 313 * 314 * @param msg te SerialMessage received from Simulator inpipe 315 * @return decimal coil ID 316 */ 317 private int getStartAddress(SerialMessage msg) { 318 int a1 = msg.getElement(5) - '0'; // StartAt char 1 319 int a2 = msg.getElement(6) - '0'; // StartAt char 2 320 int a3 = msg.getElement(7) - '0'; // StartAt char 3 321 int a4 = msg.getElement(8) - '0'; // StartAt char 4 322 return 1000 * a1 + 100 * a2 + 10 * a3 + a4; // combine a1..a4 323 } 324 325 /** 326 * Extract the number of coils to process from RC/WC message. 327 * 328 * @param msg te SerialMessage received from Simulator inpipe 329 * @return the number of consecutive coils to read/write (decimal) 330 * after starting Coil 331 */ 332 private int getNumberOfCoils(SerialMessage msg) { 333 int n1 = msg.getElement(9) - '0'; // N char 1 334 int n2 = msg.getElement(10) - '0'; // N char 2 335 return 10 * n1 + n2; // combine n1, n2 336 } 337 338 /** 339 * Write reply to output. 340 * <p> 341 * Adapted from jmri.jmrix.nce.simulator.SimulatorAdapter. 342 * 343 * @param r reply on message 344 */ 345 private void writeReply(SerialReply r) { 346 if (r == null) { 347 return; // there is no reply to be sent 348 } 349 for (int i = 0; i < r.getNumDataElements(); i++) { 350 try { 351 outpipe.writeByte((byte) r.getElement(i)); 352 } catch (java.io.IOException ex) { 353 } 354 } 355 try { 356 outpipe.flush(); 357 } catch (java.io.IOException ex) { 358 } 359 } 360 361 /** 362 * Get characters from the input source. 363 * <p> 364 * Only used in the Receive thread. 365 * 366 * @return filled message, only when the message is complete 367 * @throws IOException when presented by the input source 368 */ 369 private SerialMessage loadChars() throws java.io.IOException { 370 SerialReply reply = new SerialReply(); 371 ((MapleSystemConnectionMemo) getSystemConnectionMemo()).getTrafficController().loadChars(reply, inpipe); 372 373 // copy received "reply" to a Maple message of known length 374 SerialMessage msg = new SerialMessage(reply.getNumDataElements()); 375 for (int i = 0; i < msg.getNumDataElements(); i++) { 376 //log.debug("" + reply.getElement(i)); 377 msg.setElement(i, reply.getElement(i)); 378 } 379 log.debug("new message received"); 380 return msg; 381 } 382 383 /** 384 * Set checksum on simulated Maple Node reply. 385 * Code copied from {@link SerialMessage}#setChecksum(int) 386 * 387 * @param r the SerialReply to complete 388 * @param index element index to place 2 checksum bytes 389 * @return SerialReply with parity set 390 */ 391 public SerialReply setChecksum(SerialReply r, int index) { 392 int sum = 0; 393 for (int i = 1; i < index; i++) { 394 sum += r.getElement(i); 395 } 396 sum = sum & 0xFF; 397 398 char firstChar; 399 int firstVal = (sum / 16) & 0xF; 400 if (firstVal > 9) { 401 firstChar = (char) ('A' - 10 + firstVal); 402 } else { 403 firstChar = (char) ('0' + firstVal); 404 } 405 r.setElement(index, firstChar); 406 407 char secondChar; 408 int secondVal = sum & 0xf; 409 if (secondVal > 9) { 410 secondChar = (char) ('A' - 10 + secondVal); 411 } else { 412 secondChar = (char) ('0' + secondVal); 413 } 414 r.setElement(index + 1, secondChar); 415 return r; 416 } 417 418 int signalBankSize = 16; // theoretically: 16 419 int sensorBankSize = 64; // theoretically: 0x3F 420 javax.swing.Timer timer; 421 422 // streams to share with user class 423 private DataOutputStream pout = null; // this is provided to classes who want to write to us 424 private DataInputStream pin = null; // this is provided to classes who want data from us 425 // internal ends of the pipes 426 private DataOutputStream outpipe = null; // feed pin 427 private DataInputStream inpipe = null; // feed pout 428 429 private final static Logger log = LoggerFactory.getLogger(SimulatorAdapter.class); 430 431}