001package jmri.jmrix.easydcc.simulator;
002
003import java.io.DataInputStream;
004import java.io.DataOutputStream;
005import java.io.IOException;
006import java.io.PipedInputStream;
007import java.io.PipedOutputStream;
008import jmri.jmrix.easydcc.EasyDccMessage;
009import jmri.jmrix.easydcc.EasyDccPortController; // no special xSimulatorController
010import jmri.jmrix.easydcc.EasyDccReply;
011import jmri.jmrix.easydcc.EasyDccSystemConnectionMemo;
012import jmri.util.ImmediatePipedOutputStream;
013import org.slf4j.Logger;
014import org.slf4j.LoggerFactory;
015
016/**
017 * Provide access to a simulated EasyDCC system.
018 * <p>
019 * Currently, the EasyDCC SimulatorAdapter reacts to commands sent from the user interface
020 * with an appropriate reply message.
021 * Based on jmri.jmrix.lenz.xnetsimulator.XNetSimulatorAdapter / DCCppSimulatorAdapter 2017
022 * <p>
023 * NOTE: Some material in this file was modified from other portions of the
024 * support infrastructure.
025 *
026 * @author Paul Bender, Copyright (C) 2009-2010
027 * @author Mark Underwood, Copyright (C) 2015
028 * @author Egbert Broerse, Copyright (C) 2017
029 */
030public class SimulatorAdapter extends EasyDccPortController implements Runnable {
031
032    // private control members
033    private boolean opened = false;
034    private Thread sourceThread;
035
036    final static int SENSOR_MSG_RATE = 10;
037
038    private boolean outputBufferEmpty = true;
039    private final boolean checkBuffer = true;
040    // Simulator responses
041    char EDC_OPS = 0x4F;
042    char EDC_PROG = 0x50;
043
044    public SimulatorAdapter() {
045        super(new EasyDccSystemConnectionMemo("E", "EasyDCC Simulator")); // pass customized user name
046        setManufacturer(jmri.jmrix.easydcc.EasyDccConnectionTypeList.EASYDCC);
047    }
048
049    /**
050     * {@inheritDoc}
051     * Simulated input/output pipes.
052     */
053    @Override
054    public String openPort(String portName, String appName) {
055        try {
056            PipedOutputStream tempPipeI = new ImmediatePipedOutputStream();
057            pout = new DataOutputStream(tempPipeI);
058            inpipe = new DataInputStream(new PipedInputStream(tempPipeI));
059            PipedOutputStream tempPipeO = new ImmediatePipedOutputStream();
060            outpipe = new DataOutputStream(tempPipeO);
061            pin = new DataInputStream(new PipedInputStream(tempPipeO));
062        } catch (java.io.IOException e) {
063            log.error("init (pipe): Exception: {}", e.toString());
064        }
065        opened = true;
066        return null; // indicates OK return
067    }
068
069    /**
070     * Set if the output buffer is empty or full. This should only be set to
071     * false by external processes.
072     *
073     * @param s true if output buffer is empty; false otherwise
074     */
075    synchronized public void setOutputBufferEmpty(boolean s) {
076        outputBufferEmpty = s;
077    }
078
079    /**
080     * Can the port accept additional characters? The state of CTS determines
081     * this, as there seems to be no way to check the number of queued bytes and
082     * buffer length. This might go false for short intervals, but it might also
083     * stick off if something goes wrong.
084     *
085     * @return true if port can accept additional characters; false otherwise
086     */
087    public boolean okToSend() {
088        if (checkBuffer) {
089            log.debug("Buffer Empty: {}", outputBufferEmpty);
090            return (outputBufferEmpty);
091        } else {
092            log.debug("No Flow Control or Buffer Check");
093            return (true);
094        }
095    }
096
097    /**
098     * Set up all of the other objects to operate with an EasyDccSimulator
099     * connected to this port.
100     */
101    @Override
102    public void configure() {
103        // connect to the traffic controller, which is provided via the memo
104        log.debug("set tc for memo {}", getSystemConnectionMemo().getUserName());
105
106        getSystemConnectionMemo().getTrafficController().connectPort(this);
107
108        // do the common manager config
109        this.getSystemConnectionMemo().configureManagers();
110
111        // start the simulator
112        sourceThread = new Thread(this);
113        sourceThread.setName("EasyDCC Simulator");
114        sourceThread.setPriority(Thread.MIN_PRIORITY);
115        sourceThread.start();
116    }
117
118    /**
119     * {@inheritDoc}
120     */
121    @Override
122    public void connect() throws java.io.IOException {
123        log.debug("connect called");
124        super.connect();
125    }
126
127    // Base class methods for the EasyDccPortController simulated interface
128
129    /**
130     * {@inheritDoc}
131     */
132    @Override
133    public DataInputStream getInputStream() {
134        if (!opened || pin == null) {
135            log.error("getInputStream called before load(), stream not available");
136        }
137        log.debug("DataInputStream pin returned");
138        return pin;
139    }
140
141    /**
142     * {@inheritDoc}
143     */
144    @Override
145    public DataOutputStream getOutputStream() {
146        if (!opened || pout == null) {
147            log.error("getOutputStream called before load(), stream not available");
148        }
149        log.debug("DataOutputStream pout returned");
150        return pout;
151    }
152
153    /**
154     * {@inheritDoc}
155     */
156    @Override
157    public boolean status() {
158        return opened;
159    }
160
161    /**
162     * {@inheritDoc}
163     *
164     * @return null
165     */
166    @Override
167    public String[] validBaudRates() {
168        log.debug("validBaudRates should not have been invoked");
169        return new String[]{};
170    }
171
172    /**
173     * {@inheritDoc}
174     */
175    @Override
176    public int[] validBaudNumbers() {
177        return new int[]{};
178    }
179
180    @Override
181    public String getCurrentBaudRate() {
182        return "";
183    }
184
185    @Override
186    public String getCurrentPortName(){
187        return "";
188    }
189
190    @Override
191    public void run() { // start a new thread
192        // This thread has one task. It repeatedly reads from the input pipe
193        // and writes an appropriate response to the output pipe. This is the heart
194        // of the EasyDCC command station simulation.
195        log.info("EasyDCC Simulator Started");
196        while (true) {
197            try {
198                synchronized (this) {
199                    wait(50);
200                }
201            } catch (InterruptedException e) {
202                log.debug("interrupted, ending");
203                return;
204            }
205            EasyDccMessage m = readMessage();
206            EasyDccReply r;
207            if (log.isDebugEnabled()) {
208                StringBuilder buf = new StringBuilder();
209                if (m != null) {
210                    for (int i = 0; i < m.getNumDataElements(); i++) {
211                        buf.append(Integer.toHexString(0xFF & m.getElement(i))).append(" ");
212                    }
213                } else {
214                    buf.append("null message buffer");
215                }
216                log.trace("EasyDCC Simulator Thread received message: {}", buf); // generates a lot of traffic
217            }
218            if (m != null) {
219                r = generateReply(m);
220                writeReply(r);
221                if (log.isDebugEnabled()) {
222                    StringBuilder buf = new StringBuilder();
223                    for (int i = 0; i < r.getNumDataElements(); i++) {
224                        buf.append(Integer.toHexString(0xFF & r.getElement(i))).append(" ");
225                    }
226                    log.debug("EasyDCC Simulator Thread sent reply: {}", buf);
227                }
228            }
229        }
230    }
231
232    /**
233     * Read one incoming message from the buffer
234     * and set outputBufferEmpty to true.
235     */
236    private EasyDccMessage readMessage() {
237        EasyDccMessage msg = null;
238//        log.debug("Simulator reading message");
239        try {
240            if (inpipe != null && inpipe.available() > 0) {
241                msg = loadChars();
242            }
243        } catch (java.io.IOException e) {
244            // should do something meaningful here.
245        }
246        setOutputBufferEmpty(true);
247        return (msg);
248    }
249
250    /**
251     * This is the heart of the simulation. It translates an
252     * incoming EasyDccMessage into an outgoing EasyDccReply.
253     *
254     * As yet, not all messages receive a meaningful reply. TODO: Throttle, Program
255     */
256    private EasyDccReply generateReply(EasyDccMessage msg) {
257        log.debug("Generate Reply to message type {} (string = {})", msg.toString().charAt(0), msg.toString());
258
259        EasyDccReply reply = new EasyDccReply();
260        int i = 0;
261        char command = msg.toString().charAt(0);
262        log.debug("Message type = {}", command);
263        switch (command) {
264
265            case 'X': // eXit programming
266            case 'S': // Send packet
267            case 'D': // Dequeue packet
268            case 'Q': // Queue packet
269            case 'F': // display memory
270            case 'C': // program loCo
271                reply.setElement(i++, EDC_OPS); // capital O for Operation
272                break;
273
274            case 'P':
275            case 'M':
276                reply.setElement(i++, EDC_PROG); // capital P for Programming
277                break;
278
279            case 'E':
280                log.debug("TRACK_POWER_ON detected");
281                reply.setElement(i++, EDC_OPS); // capital O for Operation
282                break;
283
284            case 'K':
285                log.debug("TRACK_POWER_OFF detected");
286                reply.setElement(i++, EDC_OPS); // capital O for Operation
287                break;
288
289            case 'V':
290                log.debug("Read_CS_Version detected");
291                String replyString = "V999 01 01 1999";
292                reply = new EasyDccReply(replyString); // fake version number reply
293                i = replyString.length();
294//                reply.setElement(i++, 0x0d); // add CR for second reply line
295//                reply.setElement(i++, EDC_OPS); // capital O for Operation
296                break;
297
298            case 'G': // Consist
299                log.debug("Consist detected");
300                if (msg.toString().charAt(0) == 'D') { // Display consist
301                    replyString = "G" + msg.getElement(2) + msg.getElement(3) + "0000";
302                    reply = new EasyDccReply(replyString); // fake version number reply
303                    i = replyString.length();
304//                    reply.setElement(i++, 0x0d); // add CR
305                    break;
306                }
307                reply.setElement(i++, EDC_OPS); // capital O for Operation, anyway
308                break;
309
310            case 'L': // Read Loco
311                log.debug("Read Loco detected");
312                replyString = "L" + msg.getElement(1) + msg.getElement(2) + msg.getElement(3) + msg.getElement(4) + "000000";
313                reply = new EasyDccReply(replyString); // fake reply dir = 00 step = 00 F5-12=00
314                i = replyString.length();
315//                reply.setElement(i++, 0x0d); // add CR for second reply line
316//                reply.setElement(i++, EDC_OPS); // capital O for Operation, anyway
317                break;
318
319            case 'R':
320                log.debug("Read_CV detected");
321                replyString = "--";
322                reply = new EasyDccReply(replyString); // cannot read
323                i = replyString.length();
324//                reply.setElement(i++, 0x0d); // add CR for second reply line
325//                reply.setElement(i++, EDC_PROG); // capital O for Operation
326                break;
327
328            default:
329                log.debug("non-reply message detected");
330                reply.setElement(i++, '?'); // per page 2 of the EasyDCC computer
331                                          // operations manual, an invalid 
332                                          // command returns ?<CR>
333        }
334        log.debug("Reply generated = {}", reply.toString());
335        reply.setElement(i++, 0x0d); // add final CR for all replies
336        return (reply);
337    }
338
339    /**
340     * Write reply to output.
341     * <p>
342     * Copied from jmri.jmrix.nce.simulator.SimulatorAdapter.
343     *
344     * @param r reply on message
345     */
346    private void writeReply(EasyDccReply r) {
347        if (r == null) {
348            return; // there is no reply to be sent
349        }
350        for (int i = 0; i < r.getNumDataElements(); i++) {
351            try {
352                outpipe.writeByte((byte) r.getElement(i));
353            } catch (java.io.IOException ex) {
354            }
355        }
356        try {
357            outpipe.flush();
358        } catch (java.io.IOException ex) {
359        }
360    }
361
362    /**
363     * Get characters from the input source.
364     * <p>
365     * Only used in the Receive thread.
366     *
367     * @return filled message, only when the message is complete.
368     * @throws IOException when presented by the input source.
369     */
370    private EasyDccMessage loadChars() throws java.io.IOException {
371        int nchars;
372        byte[] rcvBuffer = new byte[32];
373
374        nchars = inpipe.read(rcvBuffer, 0, 32);
375        //log.debug("new message received");
376        EasyDccMessage msg = new EasyDccMessage(nchars);
377
378        for (int i = 0; i < nchars; i++) {
379            msg.setElement(i, rcvBuffer[i] & 0xFF);
380        }
381        return msg;
382    }
383
384    // streams to share with user class
385    private DataOutputStream pout = null; // this is provided to classes who want to write to us
386    private DataInputStream pin = null; // this is provided to classes who want data from us
387    // internal ends of the pipes
388    private DataOutputStream outpipe = null; // feed pin
389    private DataInputStream inpipe = null; // feed pout
390
391    private final static Logger log = LoggerFactory.getLogger(SimulatorAdapter.class);
392
393}