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