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