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}