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