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