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