001package jmri.jmrix.oaktree.simulator;
002
003import java.io.*;
004
005import org.slf4j.Logger;
006import org.slf4j.LoggerFactory;
007
008// no special xSimulatorController
009import jmri.jmrix.oaktree.*;
010import jmri.util.ImmediatePipedOutputStream;
011
012/**
013 * Provide access to a simulated OakTree system.
014 * <p>
015 * Currently, the OakTree SimulatorAdapter reacts to the following commands sent from the user
016 * interface with an appropriate reply {@link #generateReply(SerialMessage)}:
017 * <ul>
018 *     <li>Poll (length = 1, reply length = 2)
019 * </ul>
020 *
021 * Based on jmri.jmrix.oaktree.simulator.SimulatorAdapter 2018
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) 2018
029 */
030@SuppressWarnings("javadoc")
031public class SimulatorAdapter extends SerialPortController implements Runnable {
032
033    // private control members
034    private Thread sourceThread;
035
036    private boolean outputBufferEmpty = true;
037    private boolean checkBuffer = true;
038
039    /**
040     * Create a new SimulatorAdapter.
041     */
042    public SimulatorAdapter() {
043        super(new OakTreeSystemConnectionMemo("O", Bundle.getMessage("OakTreeSimulatorName"))); // pass customized user name
044        setManufacturer(jmri.jmrix.oaktree.SerialConnectionTypeList.OAK);
045    }
046
047    /**
048     * {@inheritDoc}
049     * Simulated input/output pipes.
050     */
051    @Override
052    public String openPort(String portName, String appName) {
053        try {
054            PipedOutputStream tempPipeI = new ImmediatePipedOutputStream();
055            log.debug("tempPipeI created");
056            pout = new DataOutputStream(tempPipeI);
057            inpipe = new DataInputStream(new PipedInputStream(tempPipeI));
058            log.debug("inpipe created {}", inpipe != null);
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 OakTree
099     * connected to this port.
100     */
101    @Override
102    public void configure() {
103        // connect to the traffic controller
104        log.debug("set tc for memo {}", getSystemConnectionMemo().getUserName());
105        ((OakTreeSystemConnectionMemo) getSystemConnectionMemo()).getTrafficController().connectPort(this);
106        // do the common manager config
107        ((OakTreeSystemConnectionMemo) getSystemConnectionMemo()).configureManagers();
108
109        // start the simulator
110        sourceThread = new Thread(this);
111        sourceThread.setName("OakTree Simulator");
112        sourceThread.setPriority(Thread.MIN_PRIORITY);
113        sourceThread.start();
114    }
115
116    /**
117     * {@inheritDoc}
118     */
119    @Override
120    public void connect() throws java.io.IOException {
121        log.debug("connect called");
122        super.connect();
123    }
124
125    // Base class methods for the OakTree SerialPortController simulated interface
126
127    /**
128     * {@inheritDoc}
129     */
130    @Override
131    public DataInputStream getInputStream() {
132        if (!opened || pin == null) {
133            log.error("getInputStream called before load(), stream not available");
134        }
135        log.debug("DataInputStream pin returned");
136        return pin;
137    }
138
139    /**
140     * {@inheritDoc}
141     */
142    @Override
143    public DataOutputStream getOutputStream() {
144        if (!opened || pout == null) {
145            log.error("getOutputStream called before load(), stream not available");
146        }
147        log.debug("DataOutputStream pout returned");
148        return pout;
149    }
150
151    /**
152     * {@inheritDoc}
153     * @return always true, given this SimulatorAdapter is running
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 OakTree command station simulation.
194        log.info("OakTree 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            SerialMessage m = readMessage();
205            SerialReply r;
206            if (log.isTraceEnabled()) {
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("OakTree Simulator Thread received message: {}", buf ); // generates a lot of traffic
216            }
217            if (m != null) {
218                r = generateReply(m);
219                if (r != null) { // ignore errors and null replies
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("OakTree Simulator Thread sent reply: {}", buf );
227                    }
228                }
229            }
230        }
231    }
232
233    /**
234     * Read one incoming message from the buffer
235     * and set outputBufferEmpty to true.
236     */
237    private SerialMessage readMessage() {
238        SerialMessage msg = null;
239        // log.debug("Simulator reading message"); // lots of traffic in loop
240        try {
241            if (inpipe != null && inpipe.available() > 0) {
242                msg = loadChars();
243            }
244        } catch (java.io.IOException e) {
245            // should do something meaningful here.
246        }
247        setOutputBufferEmpty(true);
248        return (msg);
249    }
250
251    /**
252     * This is the heart of the simulation. It translates an
253     * incoming SerialMessage into an outgoing SerialReply.
254     * See {@link jmri.jmrix.oaktree.SerialNode#markChanges(SerialReply)} and
255     * the (draft) OakTree <a href="../package-summary.html">Binary Message Format Summary</a>.
256     *
257     * @param msg the message received in the simulated node
258     * @return a single AokTree message to confirm the requested operation, or a series
259     * of messages for each (fictitious) node/pin/state. To ignore certain commands, return null.
260     */
261    private SerialReply generateReply(SerialMessage msg) {
262        int nodeaddr = msg.getAddr();
263        log.debug("Generate Reply to message for node {} (string = {})", nodeaddr, msg.toString());
264        SerialReply reply = new SerialReply();  // reply length is determined by highest byte added
265         switch (msg.getElement(1)) {
266             case 48: // OakTree poll message
267                 reply.setElement(0, nodeaddr);
268                 reply.setElement(1, 0x50);
269                 if (((OakTreeSystemConnectionMemo) getSystemConnectionMemo()).getTrafficController().getNode(nodeaddr) == null) {
270                     log.debug("OakTree Sim generateReply getNode({}) = null", nodeaddr);
271                 } else {
272                     if (((OakTreeSystemConnectionMemo) getSystemConnectionMemo()).getTrafficController().getNode(nodeaddr).getSensorsActive()) { // input (sensors) status reply
273                         log.debug("OakTree Sim generateReply for node {}", nodeaddr);
274                         int payload = 0b0001; // dummy stand in for sensor status report; should we fetch known state from jmri node?
275                         for (int j = 1; j < 3; j++) {
276                             payload |= j << 4;
277                             reply.setElement(j + 1, payload); // there could be > 5 elements TODO see SerialNode#markChanges
278                         }
279                     } else {
280                         return null; // prevent NPE
281                     }
282                 }
283                 log.debug("Status Reply generated {}", reply.toString());
284                 return reply;
285             default:
286                 log.debug("Message ignored");
287                 return null;
288         }
289    }
290
291    /**
292     * Write reply to output.
293     * <p>
294     * Adapted from jmri.jmrix.nce.simulator.SimulatorAdapter.
295     *
296     * @param r reply on message
297     */
298    private void writeReply(SerialReply r) {
299        if (r == null) {
300            return; // there is no reply to be sent
301        }
302        for (int i = 0; i < r.getNumDataElements(); i++) {
303            try {
304                outpipe.writeByte((byte) r.getElement(i));
305            } catch (java.io.IOException ex) {
306            }
307        }
308        try {
309            outpipe.flush();
310        } catch (java.io.IOException ex) {
311        }
312    }
313
314    /**
315     * Get characters from the input source.
316     * Length is always 5 bytes.
317     * <p>
318     * Only used in the Receive thread.
319     *
320     * @return filled message, only when the message is complete.
321     * @throws IOException when presented by the input source.
322     */
323    private SerialMessage loadChars() throws java.io.IOException {
324        int i = 1;
325        int char0;
326        byte nextByte;
327        SerialMessage msg = new SerialMessage(5);
328
329        // get 1st byte
330        try {
331            byte byte0 = readByteProtected(inpipe);
332            char0 = (byte0 & 0xFF);
333            log.debug("loadChars read {}", char0);
334            msg.setElement(0, char0); // address
335        } catch (java.io.IOException e) {
336            log.debug("loadChars aborted while reading char 0");
337            return null;
338        }
339        if (char0 > 0xFF) {
340            // skip as not a node address
341            log.debug("bit not valid as node address");
342        }
343
344        // read in remaining packets
345        for (i = 1; i < 4; i++) { // read next 4 bytes
346            log.debug("reading rest of message in simulator, element {}", i);
347            try {
348                nextByte = readByteProtected(inpipe);
349                msg.setElement(i, nextByte);
350            } catch (java.io.IOException e) {
351                log.debug("loadChars aborted after {} chars", i);
352                break;
353            }
354            log.debug("loadChars read {} (item {})", Integer.toHexString(nextByte & 0xFF), i);
355        }
356
357        log.debug("OakTree message received by simulator");
358        return msg;
359    }
360
361    /**
362     * Read a single byte, protecting against various timeouts, etc.
363     * <p>
364     * When a port is set to have a receive timeout (via the
365     * enableReceiveTimeout() method), some will return zero bytes or an
366     * EOFException at the end of the timeout. In that case, the read should be
367     * repeated to get the next real character.
368     * <p>
369     * Copied from DCCppSimulatorAdapter, byte[] from XNetSimAdapter
370     */
371    private byte readByteProtected(DataInputStream istream) throws java.io.IOException {
372        byte[] rcvBuffer = new byte[1];
373        while (true) { // loop will repeat until character found
374            int nchars;
375            nchars = istream.read(rcvBuffer, 0, 1);
376            if (nchars > 0) {
377                return rcvBuffer[0];
378            }
379        }
380    }
381
382    // streams to share with user class
383    private DataOutputStream pout = null; // this is provided to classes who want to write to us
384    private DataInputStream pin = null; // this is provided to classes who want data from us
385    // internal ends of the pipes
386    private DataOutputStream outpipe = null; // feed pin
387    private DataInputStream inpipe = null; // feed pout
388
389    private final static Logger log = LoggerFactory.getLogger(SimulatorAdapter.class);
390
391}