001package jmri.jmrix.loconet.hexfile; 002 003import java.io.*; 004 005import jmri.jmrix.loconet.LnConstants; 006import jmri.jmrix.loconet.LocoNetMessage; 007import jmri.jmrix.loconet.LocoNetSystemConnectionMemo; 008import jmri.jmrix.loconet.LnPortController; 009import jmri.jmrix.loconet.lnsvf1.Lnsv1MessageContents; 010import jmri.jmrix.loconet.lnsvf2.Lnsv2MessageContents; 011import jmri.jmrix.loconet.uhlenbrock.LncvMessageContents; 012 013import org.slf4j.Logger; 014import org.slf4j.LoggerFactory; 015 016import static jmri.jmrix.loconet.lnsvf1.Lnsv1MessageContents.Sv1Command; 017 018/** 019 * LnHexFilePort implements a LnPortController via an ASCII-hex input file. See 020 * below for the file format. There are user-level controls for send next message 021 * how long to wait between messages 022 * 023 * An object of this class should run in a thread of its own so that it can fill 024 * the output pipe as needed. 025 * 026 * The input file is expected to have one message per line. Each line can 027 * contain as many bytes as needed, each represented by two Hex characters and 028 * separated by a space. Variable whitespace is not (yet) supported. 029 * 030 * @author Bob Jacobsen Copyright (C) 2001 031 */ 032public class LnHexFilePort extends LnPortController implements Runnable { 033 034 volatile private BufferedReader sFile = null; 035 volatile private boolean exit; 036 037 public LnHexFilePort() { 038 this(new HexFileSystemConnectionMemo()); 039 } 040 041 public LnHexFilePort(LocoNetSystemConnectionMemo memo) { 042 super(memo); 043 try { 044 PipedInputStream tempPipe = new PipedInputStream(); 045 pin = new DataInputStream(tempPipe); 046 outpipe = new DataOutputStream(new PipedOutputStream(tempPipe)); 047 pout = outpipe; 048 } catch (java.io.IOException e) { 049 log.error("init (pipe): Exception: {}", e.toString()); 050 } 051 options.put("MaxSlots", // NOI18N 052 new Option(Bundle.getMessage("MaxSlots") 053 + ":", // NOI18N 054 new String[] {"5","10","21","120","400"})); 055 options.put("SensorDefaultState", // NOI18N 056 new Option(Bundle.getMessage("DefaultSensorState") 057 + ":", // NOI18N 058 new String[]{Bundle.getMessage("BeanStateUnknown"), 059 Bundle.getMessage("SensorStateInactive"), 060 Bundle.getMessage("SensorStateActive")}, true)); 061 } 062 063 /** 064 * Fill the contents from a file. 065 * 066 * @param file the file to be read 067 */ 068 public void load(File file) { 069 log.debug("file: {}", file); // NOI18N 070 // create the pipe stream for output, also store as the input stream if somebody wants to send 071 // (This will emulate the LocoNet echo) 072 try { 073 sFile = new BufferedReader(new InputStreamReader(new FileInputStream(file))); 074 } catch (Exception e) { 075 log.error("load (pipe): Exception: {}", e.toString()); // NOI18N 076 } 077 } 078 079 /** 080 * Tell this class to exit. 081 * Note that the caller must also interrupt the thread after this call. 082 */ 083 public void close() { 084 exit = true; 085 } 086 087 @Override 088 public void connect() { 089 jmri.jmrix.loconet.hexfile.HexFileFrame f 090 = new jmri.jmrix.loconet.hexfile.HexFileFrame(); 091 092 f.setAdapter(this); 093 try { 094 f.initComponents(); 095 } catch (Exception ex) { 096 log.warn("starting HexFileFrame exception: {}", ex.toString()); 097 } 098 f.configure(); 099 } 100 101 public boolean threadSuspended = false; 102 103 public synchronized void suspendReading(boolean suspended) { 104 this.threadSuspended = suspended; 105 if (! threadSuspended) notify(); 106 } 107 108 @Override 109 public void run() { // invoked in a new thread 110 log.info("LocoNet Simulator Started"); // NOI18N 111 while (true) { 112 while (sFile == null) { 113 // Wait for a file to be available. We have nothing else to do, so we can sleep 114 // until we are interrupted 115 try { 116 synchronized (this) { 117 wait(100); 118 } 119 } catch (InterruptedException e) { 120 if (exit) { 121 return; 122 } 123 log.info("LnHexFilePort.run: woken from sleep"); // NOI18N 124 if (sFile == null) { 125 log.error("LnHexFilePort.run: unexpected InterruptedException, exiting"); // NOI18N 126 Thread.currentThread().interrupt(); 127 return; 128 } 129 } 130 } 131 132 log.info("LnHexFilePort.run: changing input file..."); // NOI18N 133 134 // process the input file into the output side of pipe 135 _running = true; 136 try { 137 // Take ownership of the current file, it will automatically go out of scope 138 // when we leave this scope block. Set sFile to null so we can detect a new file 139 // being set in load() while we are running the current file. 140 BufferedReader currFile = sFile; 141 sFile = null; 142 143 String s; 144 while ((s = currFile.readLine()) != null) { 145 // this loop reads one line per turn 146 // ErrLog.msg(ErrLog.debugging, "LnHexFilePort", "run", "string=<" + s + ">"); 147 int len = s.length(); 148 for (int i = 0; i < len; i += 3) { 149 // parse as hex into integer, then convert to byte 150 int ival = Integer.valueOf(s.substring(i, i + 2), 16); 151 // send each byte to the output pipe (input to consumer) 152 byte bval = (byte) ival; 153 outpipe.writeByte(bval); 154 } 155 156 // flush the pipe so other threads can see the message 157 outpipe.flush(); 158 159 // finished that line, wait 160 synchronized (this) { 161 wait(delay); 162 } 163 // 164 // Check for suspended 165 if (threadSuspended) { 166 // yes - wait until no longer suspended 167 synchronized(this) { 168 while (threadSuspended) 169 wait(); 170 } 171 } 172 } 173 174 // here we're done processing the file 175 log.info("LnHexFilePort.run: normal finish to file"); // NOI18N 176 177 } catch (InterruptedException e) { 178 if (exit) { 179 return; 180 } 181 if (sFile != null) { // changed in another thread before the interrupt 182 log.info("LnHexFilePort.run: user selected new file"); // NOI18N 183 // swallow the exception since we have handled its intent 184 } else { 185 log.error("LnHexFilePort.run: unexpected InterruptedException, exiting"); // NOI18N 186 Thread.currentThread().interrupt(); 187 return; 188 } 189 } catch (Exception e) { 190 log.error("run: Exception: {}", e.toString()); // NOI18N 191 } 192 _running = false; 193 } 194 } 195 196 /** 197 * Provide a new message delay value, but don't allow it to go below 2 msec. 198 * 199 * @param newDelay delay, in milliseconds 200 **/ 201 public void setDelay(int newDelay) { 202 delay = Math.max(2, newDelay); 203 } 204 205 // base class methods 206 207 /** 208 * {@inheritDoc} 209 **/ 210 @Override 211 public DataInputStream getInputStream() { 212 if (pin == null) { 213 log.error("getInputStream: called before load(), stream not available"); // NOI18N 214 } 215 return pin; 216 } 217 218 /** 219 * {@inheritDoc} 220 **/ 221 @Override 222 public DataOutputStream getOutputStream() { 223 if (pout == null) { 224 log.error("getOutputStream: called before load(), stream not available"); // NOI18N 225 } 226 return pout; 227 } 228 229 /** 230 * {@inheritDoc} 231 **/ 232 @Override 233 public boolean status() { 234 return (pout != null) && (pin != null); 235 } 236 237 // to tell if we're currently putting out data 238 public boolean running() { 239 return _running; 240 } 241 242 // private data 243 private boolean _running = false; 244 245 // streams to share with user class 246 private DataOutputStream pout = null; // this is provided to classes who want to write to us 247 private DataInputStream pin = null; // this is provided to classes who want data from us 248 // internal ends of the pipes 249 private DataOutputStream outpipe = null; // feed pin 250 251 @Override 252 public boolean okToSend() { 253 return true; 254 } 255 // define operation 256 private int delay = 100; // units are milliseconds; default is quiet a busy LocoNet 257 258 @Override 259 public java.util.Vector<String> getPortNames() { 260 log.error("getPortNames should not have been invoked", new Exception()); 261 return null; 262 } 263 264 /** 265 * {@inheritDoc} 266 */ 267 @Override 268 public String openPort(String portName, String appName) { 269 log.error("openPort should not have been invoked", new Exception()); 270 return null; 271 } 272 273 @Override 274 public void configure() { 275 log.error("configure should not have been invoked"); 276 } 277 278 /** 279 * {@inheritDoc} 280 */ 281 @Override 282 public String[] validBaudRates() { 283 log.error("validBaudRates should not have been invoked", new Exception()); 284 return new String[]{}; 285 } 286 287 /** 288 * {@inheritDoc} 289 */ 290 @Override 291 public int[] validBaudNumbers() { 292 return new int[]{}; 293 } 294 295 /** 296 * Get an array of valid values for "option 3"; used to display valid 297 * options. May not be null, but may have zero entries. 298 * 299 * @return the options 300 */ 301 public String[] validOption3() { 302 return new String[]{Bundle.getMessage("HandleNormal"), 303 Bundle.getMessage("HandleSpread"), 304 Bundle.getMessage("HandleOneOnly"), 305 Bundle.getMessage("HandleBoth")}; // I18N 306 } 307 308 /** 309 * Get a String that says what Option 3 represents. May be an empty string, 310 * but will not be null 311 * 312 * @return string containing the text for "Option 3" 313 */ 314 public String option3Name() { 315 return "Turnout command handling: "; 316 } 317 318 /** 319 * Set the third port option. Only to be used after construction, but before 320 * the openPort call. 321 */ 322 @Override 323 public void configureOption3(String value) { 324 super.configureOption3(value); 325 log.debug("configureOption3: {}", value); // NOI18N 326 setTurnoutHandling(value); 327 } 328 329 private boolean simReply = false; 330 331 /** 332 * Turn on/off replying to LocoNet messages to simulate devices. 333 * @param state new state for simReplies 334 */ 335 public void simReply(boolean state) { 336 simReply = state; 337 log.debug("SimReply is {}", simReply); 338 } 339 340 public boolean simReply() { 341 return simReply; 342 } 343 344 /** 345 * Choose from a subset of hardware replies to send in HexFile simulator mode in response to specific messages. 346 * Supported message types: 347 * <ul> 348 * <li>LN SV v1 {@link jmri.jmrix.loconet.lnsvf1.Lnsv1MessageContents}</li> 349 * <li>LN SV v2 {@link jmri.jmrix.loconet.lnsvf2.Lnsv2MessageContents}</li> 350 * <li>LNCV {@link jmri.jmrix.loconet.uhlenbrock.LncvMessageContents} ReadReply</li> 351 * </ul> 352 * Listener is attached to jmri.jmrix.loconet.hexfile.HexFileFrame with GUI box to turn this option on/off 353 * 354 * @param m the message to respond to 355 * @return an appropriate reply by type and values 356 */ 357 static public LocoNetMessage generateReply(LocoNetMessage m) { 358 LocoNetMessage reply = null; 359 log.debug("generateReply for {}", m.toMonitorString()); 360 361 if (Lnsv1MessageContents.isSupportedSv1Message(m)) { 362 // LOCONET_SV1/SV0 LocoIO simulation 363 // log.debug("generate reply for LNSV1 message "); 364 Lnsv1MessageContents c = new Lnsv1MessageContents(m); 365 // log.debug("HEXFILESIM generateReply (dstL={}, subAddr={})", c.getDstL(), c.getSubAddress()); 366 if (c.getSrcL() == 0x50 && c.getCmd() == Sv1Command.getCmd(Sv1Command.SV1_READ)) { 367 if (c.getDstL() == 0) { 368 // Sv1 Probe broadcast 369 // [E5 10 50 00 01 00 02 02 00 00 10 00 00 00 00 4B] LocoBuffer => LocoIO@broadcast Query SV 2. 370 log.debug("generating LNSV1 ProbeAll broadcast reply message"); 371 int myAddr = 10; // a random but valid board address I happen to have in my roster 372 int subAddress = 1; // board sub-address 373 int dest = Lnsv1MessageContents.LNSV1_LOCOBUFFER_ADDRESS; // reply to LocoBuffer 374 int version = 123; 375 int sv = 2; 376 int val = 1; 377 reply = Lnsv1MessageContents.createSv1ReadReply(myAddr, dest, subAddress, version, sv, val); 378 } else if (c.getDstL() > 0 && c.getSubAddress() > 0) { 379 // specific Read request 380 // [E5 10 50 0C 01 00 02 09 00 00 10 03 00 00 00 4F] LocoBuffer => LocoIO@0x0C/3 Query SV 9. 381 log.debug("generating LNSV1 Read reply message"); 382 int myAddr = c.getDstL(); // a random but valid board address 383 int subAddress = c.getSubAddress(); // board sub-address 384 int dest = Lnsv1MessageContents.LNSV1_LOCOBUFFER_ADDRESS; // reply to LocoBuffer 385 int version = 120; 386 int sv = c.getSvNum(); 387 int val = (sv == 1 ? c.getDstL() : (sv == 2 ? c.getSubAddress() : 76)); 388 reply = Lnsv1MessageContents.createSv1ReadReply(myAddr, dest, subAddress, version, sv, val); 389 } else { 390 log.debug("Can't generate for unknown LNSV1 Read msg [{}]", m); 391 } 392 } else if (c.getSrcL() == 0x50 && c.getCmd() == Sv1Command.getCmd(Sv1Command.SV1_WRITE)) { 393 if (c.getDstL() == 0) { 394 // broadcast Write request SetAddress() 395 // [E5 10 50 0C 01 00 01 09 00 07 10 03 00 00 00 4B] LocoBuffer => LocoIO@0x0C/3 Write SV 9=7. 396 log.debug("generating LNSV1 broadcast Write reply message"); 397 int myAddr = 18; // a random but valid board address 398 int subAddress = 3; // board sub-address 399 int dest = Lnsv1MessageContents.LNSV1_LOCOBUFFER_ADDRESS; // reply to LocoBuffer 400 int version = 149; 401 int sv = c.getSvNum(); 402 int val = c.getSvValue(); 403 reply = Lnsv1MessageContents.createSv1WriteReply(myAddr, dest, subAddress, version, sv, val); 404 } else if (c.getDstL() > 0 && c.getSubAddress() > 0) { 405 // specific 12/3 Write request 406 // [E5 10 50 0C 01 00 01 09 00 07 10 03 00 00 00 4B] LocoBuffer => LocoIO@0x0C/3 Write SV 9=7. 407 log.debug("generating LNSV1 Write reply message"); 408 int myAddr = c.getDstL(); // a random but valid board address 409 int subAddress = c.getSubAddress(); // board sub-address 410 int dest = Lnsv1MessageContents.LNSV1_LOCOBUFFER_ADDRESS; // reply to LocoBuffer 411 int version = 106; 412 int sv = c.getSvNum(); 413 int val = c.getSvValue(); 414 reply = Lnsv1MessageContents.createSv1WriteReply(myAddr, dest, subAddress, version, sv, val); 415 } else { 416 log.debug("Can't generate for unknown LNSV1 Write msg [{}]", m); 417 } 418 } else { 419 log.debug("generate ignored LNSV1 msg [{}]", m); // no sim if not from LocoBuffer 420 } 421 } else if (Lnsv2MessageContents.isSupportedSv2Message(m)) { 422 // LOCONET_SV2 simulation 423 //log.debug("generating reply for SV2 message"); 424 Lnsv2MessageContents c = new Lnsv2MessageContents(m); 425 if (c.getDestAddr() == -1) { // Sv2 QueryAll, reply (content includes no address) 426 log.debug("generate LNSV2 query reply message"); 427 int dest = 1; // keep it simple, don't fetch src from m 428 int myId = 11; // a random value 429 int mf = 129; // Digitrax 430 int dev = 1; 431 int type = 3055; 432 int serial = 111; 433 reply = Lnsv2MessageContents.createSv2DeviceDiscoveryReply(myId, dest, mf, dev, type, serial); 434 } 435 } else if (LncvMessageContents.isSupportedLncvMessage(m)) { 436 // Uhlenbrock LOCONET_LNCV simulation 437 if (LncvMessageContents.extractMessageType(m) == LncvMessageContents.LncvCommand.LNCV_READ) { 438 // generate READ REPLY 439 reply = LncvMessageContents.createLncvReadReply(m); 440 } else if (LncvMessageContents.extractMessageType(m) == LncvMessageContents.LncvCommand.LNCV_WRITE) { 441 // generate WRITE reply LACK 442 reply = new LocoNetMessage(new int[]{LnConstants.OPC_LONG_ACK, 0x6d, 0x7f, 0x1}); 443 } else if (LncvMessageContents.extractMessageType(m) == LncvMessageContents.LncvCommand.LNCV_PROG_START) { 444 // generate STARTPROGALL reply 445 reply = LncvMessageContents.createLncvProgStartReply(m); 446 } 447 // ignore LncvMessageContents.LncvCommand.LNCV_PROG_END, no response expected 448 } 449 return reply; 450 } 451 452 private final static Logger log = LoggerFactory.getLogger(LnHexFilePort.class); 453 454}