001package jmri.jmrix.oaktree.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.oaktree.SerialMessage;
009import jmri.jmrix.oaktree.SerialPortController; // no special xSimulatorController
010import jmri.jmrix.oaktree.SerialReply;
011import jmri.jmrix.oaktree.OakTreeSystemConnectionMemo;
012import jmri.util.ImmediatePipedOutputStream;
013import org.slf4j.Logger;
014import org.slf4j.LoggerFactory;
015
016/**
017 * Provide access to a simulated OakTree system.
018 * <p>
019 * Currently, the OakTree SimulatorAdapter reacts to the following commands sent from the user
020 * interface with an appropriate reply {@link #generateReply(SerialMessage)}:
021 * <ul>
022 *     <li>Poll (length = 1, reply length = 2)
023 * </ul>
024 *
025 * Based on jmri.jmrix.oaktree.simulator.SimulatorAdapter 2018
026 * <p>
027 * NOTE: Some material in this file was modified from other portions of the
028 * support infrastructure.
029 *
030 * @author Paul Bender, Copyright (C) 2009-2010
031 * @author Mark Underwood, Copyright (C) 2015
032 * @author Egbert Broerse, Copyright (C) 2018
033 */
034public class SimulatorAdapter extends SerialPortController implements Runnable {
035
036    // private control members
037    private Thread sourceThread;
038
039    private boolean outputBufferEmpty = true;
040    private boolean checkBuffer = true;
041
042    /**
043     * Create a new SimulatorAdapter.
044     */
045    public SimulatorAdapter() {
046        super(new OakTreeSystemConnectionMemo("O", Bundle.getMessage("OakTreeSimulatorName"))); // pass customized user name
047        setManufacturer(jmri.jmrix.oaktree.SerialConnectionTypeList.OAK);
048    }
049
050    /**
051     * {@inheritDoc}
052     * Simulated input/output pipes.
053     */
054    @Override
055    public String openPort(String portName, String appName) {
056        try {
057            PipedOutputStream tempPipeI = new ImmediatePipedOutputStream();
058            log.debug("tempPipeI created");
059            pout = new DataOutputStream(tempPipeI);
060            inpipe = new DataInputStream(new PipedInputStream(tempPipeI));
061            log.debug("inpipe created {}", inpipe != null);
062            PipedOutputStream tempPipeO = new ImmediatePipedOutputStream();
063            outpipe = new DataOutputStream(tempPipeO);
064            pin = new DataInputStream(new PipedInputStream(tempPipeO));
065        } catch (java.io.IOException e) {
066            log.error("init (pipe): Exception: {}", e.toString());
067        }
068        opened = true;
069        return null; // indicates OK return
070    }
071
072    /**
073     * Set if the output buffer is empty or full. This should only be set to
074     * false by external processes.
075     *
076     * @param s true if output buffer is empty; false otherwise
077     */
078    synchronized public void setOutputBufferEmpty(boolean s) {
079        outputBufferEmpty = s;
080    }
081
082    /**
083     * Can the port accept additional characters? The state of CTS determines
084     * this, as there seems to be no way to check the number of queued bytes and
085     * buffer length. This might go false for short intervals, but it might also
086     * stick off if something goes wrong.
087     *
088     * @return true if port can accept additional characters; false otherwise
089     */
090    public boolean okToSend() {
091        if (checkBuffer) {
092            log.debug("Buffer Empty: {}", outputBufferEmpty);
093            return (outputBufferEmpty);
094        } else {
095            log.debug("No Flow Control or Buffer Check");
096            return (true);
097        }
098    }
099
100    /**
101     * Set up all of the other objects to operate with an OakTree
102     * connected to this port.
103     */
104    @Override
105    public void configure() {
106        // connect to the traffic controller
107        log.debug("set tc for memo {}", getSystemConnectionMemo().getUserName());
108        ((OakTreeSystemConnectionMemo) getSystemConnectionMemo()).getTrafficController().connectPort(this);
109        // do the common manager config
110        ((OakTreeSystemConnectionMemo) getSystemConnectionMemo()).configureManagers();
111
112        // start the simulator
113        sourceThread = new Thread(this);
114        sourceThread.setName("OakTree Simulator");
115        sourceThread.setPriority(Thread.MIN_PRIORITY);
116        sourceThread.start();
117    }
118
119    /**
120     * {@inheritDoc}
121     */
122    @Override
123    public void connect() throws java.io.IOException {
124        log.debug("connect called");
125        super.connect();
126    }
127
128    // Base class methods for the OakTree SerialPortController simulated interface
129
130    /**
131     * {@inheritDoc}
132     */
133    @Override
134    public DataInputStream getInputStream() {
135        if (!opened || pin == null) {
136            log.error("getInputStream called before load(), stream not available");
137        }
138        log.debug("DataInputStream pin returned");
139        return pin;
140    }
141
142    /**
143     * {@inheritDoc}
144     */
145    @Override
146    public DataOutputStream getOutputStream() {
147        if (!opened || pout == null) {
148            log.error("getOutputStream called before load(), stream not available");
149        }
150        log.debug("DataOutputStream pout returned");
151        return pout;
152    }
153
154    /**
155     * {@inheritDoc}
156     * @return always true, given this SimulatorAdapter is running
157     */
158    @Override
159    public boolean status() {
160        return opened;
161    }
162
163    /**
164     * {@inheritDoc}
165     *
166     * @return null
167     */
168    @Override
169    public String[] validBaudRates() {
170        log.debug("validBaudRates should not have been invoked");
171        return new String[]{};
172    }
173
174    /**
175     * {@inheritDoc}
176     */
177    @Override
178    public int[] validBaudNumbers() {
179        return new int[]{};
180    }
181
182    @Override
183    public String getCurrentBaudRate() {
184        return "";
185    }
186
187    @Override
188    public String getCurrentPortName(){
189        return "";
190    }
191
192    @Override
193    public void run() { // start a new thread
194        // This thread has one task. It repeatedly reads from the input pipe
195        // and writes an appropriate response to the output pipe. This is the heart
196        // of the OakTree command station simulation.
197        log.info("OakTree Simulator Started");
198        while (true) {
199            try {
200                synchronized (this) {
201                    wait(50);
202                }
203            } catch (InterruptedException e) {
204                log.debug("interrupted, ending");
205                return;
206            }
207            SerialMessage m = readMessage();
208            SerialReply r;
209            if (log.isTraceEnabled()) {
210                StringBuilder buf = new StringBuilder();
211                if (m != null) {
212                    for (int i = 0; i < m.getNumDataElements(); i++) {
213                        buf.append(Integer.toHexString(0xFF & m.getElement(i))).append(" ");
214                    }
215                } else {
216                    buf.append("null message buffer");
217                }
218                log.trace("OakTree Simulator Thread received message: {}", buf ); // generates a lot of traffic
219            }
220            if (m != null) {
221                r = generateReply(m);
222                if (r != null) { // ignore errors and null replies
223                    writeReply(r);
224                    if (log.isDebugEnabled()) {
225                        StringBuilder buf = new StringBuilder();
226                        for (int i = 0; i < r.getNumDataElements(); i++) {
227                            buf.append(Integer.toHexString(0xFF & r.getElement(i))).append(" ");
228                        }
229                        log.debug("OakTree Simulator Thread sent reply: {}", buf );
230                    }
231                }
232            }
233        }
234    }
235
236    /**
237     * Read one incoming message from the buffer
238     * and set outputBufferEmpty to true.
239     */
240    private SerialMessage readMessage() {
241        SerialMessage msg = null;
242        // log.debug("Simulator reading message"); // lots of traffic in loop
243        try {
244            if (inpipe != null && inpipe.available() > 0) {
245                msg = loadChars();
246            }
247        } catch (java.io.IOException e) {
248            // should do something meaningful here.
249        }
250        setOutputBufferEmpty(true);
251        return (msg);
252    }
253
254    /**
255     * This is the heart of the simulation. It translates an
256     * incoming SerialMessage into an outgoing SerialReply.
257     * See {@link jmri.jmrix.oaktree.SerialNode#markChanges(SerialReply)} and
258     * the (draft) OakTree <a href="../package-summary.html">Binary Message Format Summary</a>.
259     *
260     * @param msg the message received in the simulated node
261     * @return a single AokTree message to confirm the requested operation, or a series
262     * of messages for each (fictitious) node/pin/state. To ignore certain commands, return null.
263     */
264    private SerialReply generateReply(SerialMessage msg) {
265        int nodeaddr = msg.getAddr();
266        log.debug("Generate Reply to message for node {} (string = {})", nodeaddr, msg.toString());
267        SerialReply reply = new SerialReply();  // reply length is determined by highest byte added
268         switch (msg.getElement(1)) {
269             case 48: // OakTree poll message
270                 reply.setElement(0, nodeaddr);
271                 reply.setElement(1, 0x50);
272                 if (((OakTreeSystemConnectionMemo) getSystemConnectionMemo()).getTrafficController().getNode(nodeaddr) == null) {
273                     log.debug("OakTree Sim generateReply getNode({}) = null", nodeaddr);
274                 } else {
275                     if (((OakTreeSystemConnectionMemo) getSystemConnectionMemo()).getTrafficController().getNode(nodeaddr).getSensorsActive()) { // input (sensors) status reply
276                         log.debug("OakTree Sim generateReply for node {}", nodeaddr);
277                         int payload = 0b0001; // dummy stand in for sensor status report; should we fetch known state from jmri node?
278                         for (int j = 1; j < 3; j++) {
279                             payload |= j << 4;
280                             reply.setElement(j + 1, payload); // there could be > 5 elements TODO see SerialNode#markChanges
281                         }
282                     } else {
283                         return null; // prevent NPE
284                     }
285                 }
286                 log.debug("Status Reply generated {}", reply.toString());
287                 return reply;
288             default:
289                 log.debug("Message ignored");
290                 return null;
291         }
292    }
293
294    /**
295     * Write reply to output.
296     * <p>
297     * Adapted from jmri.jmrix.nce.simulator.SimulatorAdapter.
298     *
299     * @param r reply on message
300     */
301    private void writeReply(SerialReply r) {
302        if (r == null) {
303            return; // there is no reply to be sent
304        }
305        for (int i = 0; i < r.getNumDataElements(); i++) {
306            try {
307                outpipe.writeByte((byte) r.getElement(i));
308            } catch (java.io.IOException ex) {
309            }
310        }
311        try {
312            outpipe.flush();
313        } catch (java.io.IOException ex) {
314        }
315    }
316
317    /**
318     * Get characters from the input source.
319     * Length is always 5 bytes.
320     * <p>
321     * Only used in the Receive thread.
322     *
323     * @return filled message, only when the message is complete.
324     * @throws IOException when presented by the input source.
325     */
326    private SerialMessage loadChars() throws java.io.IOException {
327        int i = 1;
328        int char0;
329        byte nextByte;
330        SerialMessage msg = new SerialMessage(5);
331
332        // get 1st byte
333        try {
334            byte byte0 = readByteProtected(inpipe);
335            char0 = (byte0 & 0xFF);
336            log.debug("loadChars read {}", char0);
337            msg.setElement(0, char0); // address
338        } catch (java.io.IOException e) {
339            log.debug("loadChars aborted while reading char 0");
340            return null;
341        }
342        if (char0 > 0xFF) {
343            // skip as not a node address
344            log.debug("bit not valid as node address");
345        }
346
347        // read in remaining packets
348        for (i = 1; i < 4; i++) { // read next 4 bytes
349            log.debug("reading rest of message in simulator, element {}", i);
350            try {
351                nextByte = readByteProtected(inpipe);
352                msg.setElement(i, nextByte);
353            } catch (java.io.IOException e) {
354                log.debug("loadChars aborted after {} chars", i);
355                break;
356            }
357            log.debug("loadChars read {} (item {})", Integer.toHexString(nextByte & 0xFF), i);
358        }
359
360        log.debug("OakTree message received by simulator");
361        return msg;
362    }
363
364    /**
365     * Read a single byte, protecting against various timeouts, etc.
366     * <p>
367     * When a port is set to have a receive timeout (via the
368     * enableReceiveTimeout() method), some will return zero bytes or an
369     * EOFException at the end of the timeout. In that case, the read should be
370     * repeated to get the next real character.
371     * <p>
372     * Copied from DCCppSimulatorAdapter, byte[] from XNetSimAdapter
373     */
374    private byte readByteProtected(DataInputStream istream) throws java.io.IOException {
375        byte[] rcvBuffer = new byte[1];
376        while (true) { // loop will repeat until character found
377            int nchars;
378            nchars = istream.read(rcvBuffer, 0, 1);
379            if (nchars > 0) {
380                return rcvBuffer[0];
381            }
382        }
383    }
384
385    // streams to share with user class
386    private DataOutputStream pout = null; // this is provided to classes who want to write to us
387    private DataInputStream pin = null; // this is provided to classes who want data from us
388    // internal ends of the pipes
389    private DataOutputStream outpipe = null; // feed pin
390    private DataInputStream inpipe = null; // feed pout
391
392    private final static Logger log = LoggerFactory.getLogger(SimulatorAdapter.class);
393
394}