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(1000);
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}