001package jmri.jmrix.secsi.simulator;
002
003import java.io.*;
004
005import org.slf4j.Logger;
006import org.slf4j.LoggerFactory;
007
008// no special xSimulatorController
009import jmri.jmrix.secsi.*;
010import jmri.util.ImmediatePipedOutputStream;
011
012/**
013 * Provide access to a simulated SECSI system.
014 * <p>
015 * Currently, the Secsi 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.grapevine.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 SecsiSystemConnectionMemo("V", Bundle.getMessage("SecsiSimulatorName"))); // pass customized user name
044        setManufacturer(jmri.jmrix.secsi.SerialConnectionTypeList.TRACTRONICS);
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 a SECSI
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        ((SecsiSystemConnectionMemo) getSystemConnectionMemo()).getTrafficController().connectPort(this);
106        // do the common manager config
107        ((SecsiSystemConnectionMemo) getSystemConnectionMemo()).configureManagers();
108
109        // start the simulator
110        sourceThread = new Thread(this);
111        sourceThread.setName("Secsi 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 SECSI 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 Secsi command station simulation.
194        log.info("Secsi 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("Secsi 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("Secsi 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    // operational instance variable (not preserved between runs)
252    protected boolean[] nodesSet = new boolean[128]; // node init received and replied?
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.secsi.SerialNode#markChanges(SerialReply)} and
258     * the (draft) secsi <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 Secsi 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//        if (nodesSet[nodeaddr] != true) { // only Polls expect a reply from the node
269         switch (msg.getNumDataElements()) {
270             case 1: // poll message, but reading msg received often fails (see case 9)
271                 log.debug("Poll message detected by simulator");
272                 reply.setElement(0, nodeaddr); // node address from msg element(0)
273                 reply.setElement(1, 0x30); // poll reply contains just 2 elements, second is 0x48 (see SerialMessage#isPoll())
274                 nodesSet[nodeaddr] = true; // mark node as inited
275                 log.debug("Poll reply generated {}", reply.toString());
276                 return reply;
277             case 5: // standard secsi sensor state request message
278                 if (((SecsiSystemConnectionMemo) getSystemConnectionMemo()).getTrafficController().getNode(nodeaddr).getSensorsActive()) { // input (sensors) status reply
279                     int payload = 0b0101; // dummy stand in for sensor status report; should we fetch known state from jmri node?
280                     for (int j = 0; j < 3; j++) {
281                         payload |= j << 4;
282                         reply.setElement(j + 1, payload);
283                     }
284                     log.debug("Status Reply generated {}", reply.toString());
285                 }
286                 return reply;
287             case 9:
288                 // use this message to confirm node poll?
289                 //reply.setElement(0, nodeaddr); // node address from msg element(0)
290                 //reply.setElement(1, 48); // poll reply contains just 2 elements, second is 0x48 (see SerialMessage#isPoll())
291                 log.debug("Outpacket received"); // Poll Reply generated: {}", reply.toString());
292                 return null; // reply;
293             default:
294                 log.debug("Message (other) ignored");
295                 return null;
296         }
297        // Poll will give an error:
298        // jmrix.AbstractMRTrafficController ERROR - Transmit thread terminated prematurely by:
299        // java.lang.ArrayIndexOutOfBoundsException: 1 [secsi.SerialTrafficController Transmit thread]
300    }
301
302    /**
303     * Write reply to output.
304     * <p>
305     * Adapted from jmri.jmrix.nce.simulator.SimulatorAdapter.
306     *
307     * @param r reply on message
308     */
309    private void writeReply(SerialReply r) {
310        if (r == null) {
311            return; // there is no reply to be sent
312        }
313        for (int i = 0; i < r.getNumDataElements(); i++) {
314            try {
315                outpipe.writeByte((byte) r.getElement(i));
316            } catch (java.io.IOException ex) {
317            }
318        }
319        try {
320            outpipe.flush();
321        } catch (java.io.IOException ex) {
322        }
323    }
324
325    private int[] lastChars = new int[9]; // temporary store of bytes received, excluding node address
326    private int nextNodeAddress;
327    private boolean lastCharLoaded = false;
328
329    /**
330     * Get characters from the input source. No opcode, so must read per byte.
331     * Length will be either 1, 5 or 9 bytes.
332     * <p>
333     * Only used in the Receive thread.
334     *
335     * @return filled message, only when the message is complete.
336     * @throws IOException when presented by the input source.
337     */
338    private SerialMessage loadChars() throws java.io.IOException {
339        int i = 1;
340        int char0;
341        byte nextByte;
342
343        // get 1st byte, see if ending too soon
344        if (lastCharLoaded && (nextNodeAddress < 0x2F)) { // use char previously read fom pipe as element 0 (node address)
345            char0 = nextNodeAddress;
346            lastCharLoaded = false;
347        } else {
348            try {
349                byte byte0 = readByteProtected(inpipe);
350                char0 = (byte0 & 0xFF);
351                log.debug("loadChars read {}", char0);
352            } catch (java.io.IOException e) {
353                lastCharLoaded = false; // we lost track
354                log.debug("loadChars aborted while reading char 0");
355                return null;
356            }
357        }
358        if (char0 > 0x2F) {
359            // skip as not a node address
360            log.debug("bit not valid as node address");
361        }
362
363        // try if what is received is a series of outpackets
364        for (i = 1; i < 9; i++) { // reading next max 8 bytes
365            log.debug("reading rest of message in simulator, element {}", i);
366            try {
367                nextByte = readByteProtected(inpipe);
368            } catch (java.io.IOException e) {
369                log.debug("loadChars aborted after {} chars", i);
370                lastCharLoaded = false; // we lost track
371                //i = i - 1; // current message complete at previous char
372                log.debug("overshot reading Secsi message at element {}. Ready", i);
373                break;
374            }
375            log.debug("loadChars read {} (item {})", Integer.toHexString(nextByte & 0xFF), i);
376            // check if it is one of the 8 byte 0x .. 7x Outpackets series
377            if ((nextByte & 0xFF) >> 4 == i - 1) { // pattern for next element in range of increasing 0x .. 7x Outpackets
378                lastChars[i] = (nextByte & 0xFF);
379                log.debug("matched item {} in series: {}", i, (nextByte & 0xFF) >> 4);
380            } else if ((nextByte & 0xFF) < 0x2F) { // if it's not, store last item read as first element of next message
381                // nextChar could be node address again, in that case the preceding was perhaps a single node poll message
382                // but on node 00 could follow the first of the outputpacket series 00 10 etc.
383                nextNodeAddress = (nextByte & 0xFF); // store value in array
384                lastCharLoaded = true;
385                i = Math.max(1, i - 1); // current message complete at previous char
386                log.debug("overshot reading Secsi message at element {}. Next node = {}", i, nextNodeAddress);
387                break;
388            } else { // we lost this series, but previous item could have been the next new node address
389                if ((lastChars[i - 1] >= 0) && (lastChars[i - 1] < 0x2F)) { // valid as node address
390                    nextNodeAddress = lastChars[i - 1];
391                    lastCharLoaded = true; // store last byte read as possible next node address
392                    i = Math.max(1, i - 1); // current message complete before previous char
393                    log.debug("overshot Secsi message at element {}. Next node = {}", i, nextNodeAddress);
394                    break;
395                } else { // unhandled message type
396                    lastCharLoaded = false; // discard last byte read as not making sense
397                    i = Math.max(1, i - 1); // current message complete at previous char
398                    log.debug("unhandled Secsi message from element {}", i);
399                    break;
400                }
401            }
402        }
403
404        // copy bytes to Message
405        SerialMessage msg = new SerialMessage(i);
406        msg.setElement(0, char0); // address
407        for (int k = 1; k < i; k++) { // copy remaining bytes if i > 1
408            msg.setElement(k, lastChars[k]);
409        }
410        log.debug("Secsi message received by simulator, length = {}", i);
411        if (msg.getNumDataElements() == 1) {
412            nodesSet[char0] = false; // reset first node poll
413        }
414        return msg;
415    }
416
417    /**
418     * Read a single byte, protecting against various timeouts, etc.
419     * <p>
420     * When a port is set to have a receive timeout (via the
421     * enableReceiveTimeout() method), some will return zero bytes or an
422     * EOFException at the end of the timeout. In that case, the read should be
423     * repeated to get the next real character.
424     * <p>
425     * Copied from DCCppSimulatorAdapter, byte[] from XNetSimAdapter
426     */
427    private byte readByteProtected(DataInputStream istream) throws java.io.IOException {
428        byte[] rcvBuffer = new byte[1];
429        while (true) { // loop will repeat until character found
430            int nchars;
431            nchars = istream.read(rcvBuffer, 0, 1);
432            if (nchars > 0) {
433                return rcvBuffer[0];
434            }
435        }
436    }
437
438    // streams to share with user class
439    private DataOutputStream pout = null; // this is provided to classes who want to write to us
440    private DataInputStream pin = null; // this is provided to classes who want data from us
441    // internal ends of the pipes
442    private DataOutputStream outpipe = null; // feed pin
443    private DataInputStream inpipe = null; // feed pout
444
445    private final static Logger log = LoggerFactory.getLogger(SimulatorAdapter.class);
446
447}