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    @Override
089    public void run() { // invoked in a new thread
090        log.info("LocoNet Simulator Started"); // NOI18N
091        while (true) {
092            while (sFile == null) {
093                // Wait for a file to be available. We have nothing else to do, so we can sleep
094                // until we are interrupted
095                try {
096                    synchronized (this) {
097                        wait(1000);
098                    }
099                } catch (InterruptedException e) {
100                    log.info("LnHexFilePort.run: woken from sleep"); // NOI18N
101                    if (sFile == null) {
102                        log.error("LnHexFilePort.run: unexpected InterruptedException, exiting"); // NOI18N
103                        Thread.currentThread().interrupt();
104                        return;
105                    }
106                }
107            }
108
109            log.info("LnHexFilePort.run: changing input file..."); // NOI18N
110
111            // process the input file into the output side of pipe
112            _running = true;
113            try {
114                // Take ownership of the current file, it will automatically go out of scope
115                // when we leave this scope block.  Set sFile to null so we can detect a new file
116                // being set in load() while we are running the current file.
117                BufferedReader currFile = sFile;
118                sFile = null;
119
120                String s;
121                while ((s = currFile.readLine()) != null) {
122                    // this loop reads one line per turn
123                    // ErrLog.msg(ErrLog.debugging, "LnHexFilePort", "run", "string=<" + s + ">");
124                    int len = s.length();
125                    for (int i = 0; i < len; i += 3) {
126                        // parse as hex into integer, then convert to byte
127                        int ival = Integer.valueOf(s.substring(i, i + 2), 16);
128                        // send each byte to the output pipe (input to consumer)
129                        byte bval = (byte) ival;
130                        outpipe.writeByte(bval);
131                    }
132
133                    // flush the pipe so other threads can see the message
134                    outpipe.flush();
135
136                    // finished that line, wait
137                    synchronized (this) {
138                        wait(delay);
139                    }
140                    //Thread.sleep(delay);
141                }
142
143                // here we're done processing the file
144                log.info("LnHexFilePort.run: normal finish to file"); // NOI18N
145
146            } catch (InterruptedException e) {
147                if (sFile != null) { // changed in another thread before the interrupt
148                    log.info("LnHexFilePort.run: user selected new file"); // NOI18N
149                    // swallow the exception since we have handled its intent
150                } else {
151                    log.error("LnHexFilePort.run: unexpected InterruptedException, exiting"); // NOI18N
152                    Thread.currentThread().interrupt();
153                    return;
154                }
155            } catch (Exception e) {
156                log.error("run: Exception: {}", e.toString()); // NOI18N
157            }
158            _running = false;
159        }
160    }
161
162    /**
163     * Provide a new message delay value, but don't allow it to go below 2 msec.
164     *
165     * @param newDelay delay, in milliseconds
166     **/
167    public void setDelay(int newDelay) {
168        delay = Math.max(2, newDelay);
169    }
170
171    // base class methods
172
173    /**
174     * {@inheritDoc}
175     **/
176    @Override
177    public DataInputStream getInputStream() {
178        if (pin == null) {
179            log.error("getInputStream: called before load(), stream not available"); // NOI18N
180        }
181        return pin;
182    }
183
184    /**
185     * {@inheritDoc}
186     **/
187    @Override
188    public DataOutputStream getOutputStream() {
189        if (pout == null) {
190            log.error("getOutputStream: called before load(), stream not available"); // NOI18N
191        }
192        return pout;
193    }
194
195    /**
196     * {@inheritDoc}
197     **/
198    @Override
199    public boolean status() {
200        return (pout != null) && (pin != null);
201    }
202
203    // to tell if we're currently putting out data
204    public boolean running() {
205        return _running;
206    }
207
208    // private data
209    private boolean _running = false;
210
211    // streams to share with user class
212    private DataOutputStream pout = null; // this is provided to classes who want to write to us
213    private DataInputStream pin = null;  // this is provided to classes who want data from us
214    // internal ends of the pipes
215    private DataOutputStream outpipe = null;  // feed pin
216
217    @Override
218    public boolean okToSend() {
219        return true;
220    }
221    // define operation
222    private int delay = 100;      // units are milliseconds; default is quiet a busy LocoNet
223
224    @Override
225    public java.util.Vector<String> getPortNames() {
226        log.error("getPortNames should not have been invoked", new Exception());
227        return null;
228    }
229
230    /**
231     * {@inheritDoc}
232     */
233    @Override
234    public String openPort(String portName, String appName) {
235        log.error("openPort should not have been invoked", new Exception());
236        return null;
237    }
238
239    @Override
240    public void configure() {
241        log.error("configure should not have been invoked");
242    }
243
244    /**
245     * {@inheritDoc}
246     */
247    @Override
248    public String[] validBaudRates() {
249        log.error("validBaudRates should not have been invoked", new Exception());
250        return new String[]{};
251    }
252
253    /**
254     * {@inheritDoc}
255     */
256    @Override
257    public int[] validBaudNumbers() {
258        return new int[]{};
259    }
260
261    /**
262     * Get an array of valid values for "option 3"; used to display valid
263     * options. May not be null, but may have zero entries.
264     *
265     * @return the options
266     */
267    public String[] validOption3() {
268        return new String[]{Bundle.getMessage("HandleNormal"),
269                Bundle.getMessage("HandleSpread"),
270                Bundle.getMessage("HandleOneOnly"),
271                Bundle.getMessage("HandleBoth")}; // I18N
272    }
273
274    /**
275     * Get a String that says what Option 3 represents. May be an empty string,
276     * but will not be null
277     *
278     * @return string containing the text for "Option 3"
279     */
280    public String option3Name() {
281        return "Turnout command handling: ";
282    }
283
284    /**
285     * Set the third port option. Only to be used after construction, but before
286     * the openPort call.
287     */
288    @Override
289    public void configureOption3(String value) {
290        super.configureOption3(value);
291        log.debug("configureOption3: {}", value); // NOI18N
292        setTurnoutHandling(value);
293    }
294
295    private boolean simReply = false;
296
297    /**
298     * Turn on/off replying to LocoNet messages to simulate devices.
299     * @param state new state for simReplies
300     */
301    public void simReply(boolean state) {
302        simReply = state;
303        log.debug("SimReply is {}", simReply);
304    }
305
306    public boolean simReply() {
307        return simReply;
308    }
309
310    /**
311     * Choose from a subset of hardware replies to send in HexFile simulator mode in response to specific messages.
312     * Supported message types:
313     * <ul>
314     *     <li>LN SV rev2 {@link jmri.jmrix.loconet.lnsvf2.LnSv2MessageContents}</li>
315     *     <li>LNCV {@link jmri.jmrix.loconet.uhlenbrock.LncvMessageContents} ReadReply</li>
316     * </ul>
317     * Listener is attached to jmri.jmrix.loconet.hexfile.HexFileFrame with GUI box to turn this option on/off
318     *
319     * @param m the message to respond to
320     * @return an appropriate reply by type and values
321     */
322    static public LocoNetMessage generateReply(LocoNetMessage m) {
323        LocoNetMessage reply = null;
324        //log.debug("generateReply for {}", m.toMonitorString());
325
326        if (LnSv2MessageContents.isSupportedSv2Message(m)) {
327            //log.debug("generate reply for SV2 message");
328            LnSv2MessageContents c = new LnSv2MessageContents(m);
329            if (c.getDestAddr() == -1) { // Sv2 QueryAll, reply (content includes no address)
330                log.debug("generate LNSV2 query reply message");
331                int dest = 1; // keep it simple, don't fetch src from m
332                int myId = 11; // a random value
333                int mf = 129; // Digitrax
334                int dev = 1;
335                int type = 3055;
336                int serial = 111;
337                reply = LnSv2MessageContents.createSv2DeviceDiscoveryReply(myId, dest, mf, dev, type, serial);
338            }
339        } else if (LncvMessageContents.isSupportedLncvMessage(m)) {
340            if (LncvMessageContents.extractMessageType(m) == LncvMessageContents.LncvCommand.LNCV_READ) {
341                // generate READ REPLY
342                reply = LncvMessageContents.createLncvReadReply(m);
343            } else if (LncvMessageContents.extractMessageType(m) == LncvMessageContents.LncvCommand.LNCV_WRITE) {
344                // generate WRITE reply LACK
345                reply = new LocoNetMessage(new int[]{LnConstants.OPC_LONG_ACK, 0x6d, 0x7f, 0x1});
346            } else if (LncvMessageContents.extractMessageType(m) == LncvMessageContents.LncvCommand.LNCV_PROG_START) {
347                // generate STARTPROGALL reply
348                reply = LncvMessageContents.createLncvProgStartReply(m);
349            }
350            // ignore LncvMessageContents.LncvCommand.LNCV_PROG_END, no response expected
351        }
352        return reply;
353    }
354
355    private final static Logger log = LoggerFactory.getLogger(LnHexFilePort.class);
356
357}