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