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