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}