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}