001package jmri.jmrix.maple.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.maple.SerialMessage;
009import jmri.jmrix.maple.SerialPortController; // no special xSimulatorController
010import jmri.jmrix.maple.SerialReply;
011import jmri.jmrix.maple.MapleSystemConnectionMemo;
012import jmri.util.ImmediatePipedOutputStream;
013import org.slf4j.Logger;
014import org.slf4j.LoggerFactory;
015
016/**
017 * Provide access to a simulated Maple system.
018 * <p>
019 * Currently, the Maple SimulatorAdapter reacts to the following commands sent from the user
020 * interface with an appropriate reply {@link #generateReply(SerialMessage)}:
021 * <ul>
022 *     <li>RC Read Coils (poll), all coil bits 0
023 *     <li>WC Write Coils (ACK)
024 * </ul>
025 *
026 * Based on jmri.jmrix.lenz.xnetsimulator.XNetSimulatorAdapter / GrapevineSimulatorAdapter 2017
027 * <p>
028 * NOTE: Some material in this file was modified from other portions of the
029 * support infrastructure.
030 *
031 * @author Paul Bender, Copyright (C) 2009-2010
032 * @author Mark Underwood, Copyright (C) 2015
033 * @author Egbert Broerse, Copyright (C) 2018
034 */
035public class SimulatorAdapter extends SerialPortController implements Runnable {
036
037    // private control members
038    private boolean opened = false;
039    private Thread sourceThread;
040
041    private boolean outputBufferEmpty = true;
042    private boolean checkBuffer = true;
043
044    /**
045     * Create a new SimulatorAdapter.
046     */
047    public SimulatorAdapter() {
048        super(new MapleSystemConnectionMemo("K", Bundle.getMessage("MapleSimulatorName"))); // pass customized user name
049        setManufacturer(jmri.jmrix.maple.SerialConnectionTypeList.MAPLE);
050    }
051
052    /**
053     * {@inheritDoc}
054     * Simulated input/output pipes.
055     */
056    @Override
057    public String openPort(String portName, String appName) {
058        try {
059            PipedOutputStream tempPipeI = new ImmediatePipedOutputStream();
060            log.debug("tempPipeI created");
061            pout = new DataOutputStream(tempPipeI);
062            inpipe = new DataInputStream(new PipedInputStream(tempPipeI));
063            log.debug("inpipe created {}", inpipe != null);
064            PipedOutputStream tempPipeO = new ImmediatePipedOutputStream();
065            outpipe = new DataOutputStream(tempPipeO);
066            pin = new DataInputStream(new PipedInputStream(tempPipeO));
067        } catch (java.io.IOException e) {
068            log.error("init (pipe): Exception: {}", e.toString());
069        }
070        opened = true;
071        return null; // indicates OK return
072    }
073
074    /**
075     * Set if the output buffer is empty or full. This should only be set to
076     * false by external processes.
077     *
078     * @param s true if output buffer is empty; false otherwise
079     */
080    synchronized public void setOutputBufferEmpty(boolean s) {
081        outputBufferEmpty = s;
082    }
083
084    /**
085     * Can the port accept additional characters? The state of CTS determines
086     * this, as there seems to be no way to check the number of queued bytes and
087     * buffer length. This might go false for short intervals, but it might also
088     * stick off if something goes wrong.
089     *
090     * @return true if port can accept additional characters; false otherwise
091     */
092    public boolean okToSend() {
093        if (checkBuffer) {
094            log.debug("Buffer Empty: {}", outputBufferEmpty);
095            return (outputBufferEmpty);
096        } else {
097            log.debug("No Flow Control or Buffer Check");
098            return (true);
099        }
100    }
101
102    /**
103     * Set up all of the other objects to operate with a MapleSimulator
104     * connected to this port.
105     */
106    @Override
107    public void configure() {
108        log.debug("set tc for memo {}", getSystemConnectionMemo().getUserName());
109        // connect to the traffic controller
110        ((MapleSystemConnectionMemo) getSystemConnectionMemo()).getTrafficController().connectPort(this);
111        // do the common manager config
112        ((MapleSystemConnectionMemo) getSystemConnectionMemo()).configureManagers();
113
114        // start the simulator
115        sourceThread = new Thread(this);
116        sourceThread.setName("Maple Simulator");
117        sourceThread.setPriority(Thread.MIN_PRIORITY);
118        sourceThread.start();
119    }
120
121    /**
122     * {@inheritDoc}
123     */
124    @Override
125    public void connect() throws java.io.IOException {
126        log.debug("connect called");
127        super.connect();
128    }
129
130    // Base class methods for the Maple SerialPortController simulated interface
131
132    /**
133     * {@inheritDoc}
134     */
135    @Override
136    public DataInputStream getInputStream() {
137        if (!opened || pin == null) {
138            log.error("getInputStream called before load(), stream not available");
139        }
140        log.debug("DataInputStream pin returned");
141        return pin;
142    }
143
144    /**
145     * {@inheritDoc}
146     */
147    @Override
148    public DataOutputStream getOutputStream() {
149        if (!opened || pout == null) {
150            log.error("getOutputStream called before load(), stream not available");
151        }
152        log.debug("DataOutputStream pout returned");
153        return pout;
154    }
155
156    /**
157     * {@inheritDoc}
158     * @return always true, given this SimulatorAdapter is running
159     */
160    @Override
161    public boolean status() {
162        return opened;
163    }
164
165    /**
166     * {@inheritDoc}
167     *
168     * @return null
169     */
170    @Override
171    public String[] validBaudRates() {
172        log.debug("validBaudRates should not have been invoked");
173        return new String[]{};
174    }
175
176    /**
177     * {@inheritDoc}
178     */
179    @Override
180    public int[] validBaudNumbers() {
181        return new int[]{};
182    }
183
184    @Override
185    public String getCurrentBaudRate() {
186        return "";
187    }
188
189    @Override
190    public String getCurrentPortName(){
191        return "";
192    }
193
194    @Override
195    public void run() { // start a new thread
196        // This thread has one task. It repeatedly reads from the input pipe
197        // and writes an appropriate response to the output pipe. This is the heart
198        // of the Maple command station simulation.
199        log.info("Maple Simulator Started");
200        while (true) {
201            try {
202                synchronized (this) {
203                    wait(50);
204                }
205            } catch (InterruptedException e) {
206                log.debug("interrupted, ending");
207                return;
208            }
209            SerialMessage m = readMessage();
210            SerialReply r;
211            if (log.isTraceEnabled()) {
212                StringBuilder buf = new StringBuilder();
213                if (m != null) {
214                    for (int i = 0; i < m.getNumDataElements(); i++) {
215                        buf.append(Integer.toHexString(0xFF & m.getElement(i))).append(" ");
216                    }
217                } else {
218                    buf.append("null message buffer");
219                }
220                log.trace("Maple Simulator Thread received message: {}", buf); // generates a lot of traffic
221            }
222            if (m != null) {
223                r = generateReply(m);
224                if (r != null) { // ignore errors
225                    writeReply(r);
226                    if (log.isDebugEnabled()) {
227                        StringBuilder buf = new StringBuilder();
228                        for (int i = 0; i < r.getNumDataElements(); i++) {
229                            buf.append(Integer.toHexString(0xFF & r.getElement(i))).append(" ");
230                        }
231                        log.debug("Maple Simulator Thread sent reply: {}", buf);
232                    }
233                }
234            }
235        }
236    }
237
238    /**
239     * Read one incoming message from the buffer
240     * and set outputBufferEmpty to true.
241     */
242    private SerialMessage readMessage() {
243        SerialMessage msg = null;
244        // log.debug("Simulator reading message");
245        try {
246            if (inpipe != null && inpipe.available() > 0) {
247                msg = loadChars();
248            }
249        } catch (java.io.IOException e) {
250            // should do something meaningful here.
251        }
252        setOutputBufferEmpty(true);
253        return (msg);
254    }
255
256    /**
257     * This is the heart of the simulation. It translates an
258     * incoming SerialMessage into an outgoing SerialReply.
259     * See {@link jmri.jmrix.maple.SerialMessage}.
260     *
261     * @param msg the message received in the simulated node
262     * @return a single Maple 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        log.debug("Generate Reply to message from node {} (string = {})", msg.getAddress(), msg.toString());
267
268        SerialReply reply = new SerialReply(); // reply length is determined by highest byte added
269        int nodeAddress = msg.getUA();         // node addres from element 1 + 2
270        //convert hex to character
271        char cmd1 = (char) msg.getElement(3);  // command char 1
272        char cmd2 = (char) msg.getElement(4);  // command char 2
273
274        log.debug("Message nodeaddress={} cmd={}{}, Start={}, Num={}",
275                nodeAddress, cmd1, cmd2,
276                getStartAddress(msg), getNumberOfCoils(msg));
277
278        switch ("" + cmd1 + cmd2) {
279            case "RC": // Read Coils message
280                log.debug("Read Coils (poll) message detected");
281                int i = 1;
282                // init reply
283                log.debug("RC Reply from node {}", nodeAddress);
284                reply.setElement(0, 0x02); // <STX>
285                reply.setElement(1, msg.getElement(1));
286                reply.setElement(2, msg.getElement(2));
287                reply.setElement(3, 'R');
288                reply.setElement(4, 'C');
289                for (i = 1; i < getNumberOfCoils(msg); i++) {
290                    reply.setElement(i + 4, 0x00); // report state of each requested coil as Inactive = 0
291                    // TODO: echo commanded state from JMRI node-bit using: getCommandedState(nodeAddress * 1000 + getStartAddress(msg) + 1)
292                }
293                reply.setElement(i + 5, 0x03);
294                reply = setChecksum(reply, i + 6);
295                break;
296            case "WC": // Write Coils message
297                log.debug("Write Coils message detected");
298                // init reply
299                log.debug("WC Reply from node {}", nodeAddress);
300                reply.setElement(0, 0x06); // <ACK>
301                reply.setElement(1, msg.getElement(1));
302                reply.setElement(2, msg.getElement(2));
303                reply.setElement(3, 'W');
304                reply.setElement(4, 'C');
305                break;
306            default:
307                // TODO "WC" message replies
308                log.debug("command ignored");
309                reply = null; // ignore all other messages
310        }
311        log.debug("Reply {}", reply == null ? "empty, Message ignored" : "generated " + reply.toString());
312        return (reply);
313    }
314
315    /**
316     * Extract start coils from RC/WC message.
317     *
318     * @param msg te SerialMessage received from Simulator inpipe
319     * @return decimal coil ID
320     */
321    private int getStartAddress(SerialMessage msg) {
322        int a1 = msg.getElement(5) - '0';  // StartAt char 1
323        int a2 = msg.getElement(6) - '0';  // StartAt char 2
324        int a3 = msg.getElement(7) - '0';  // StartAt char 3
325        int a4 = msg.getElement(8) - '0';  // StartAt char 4
326        return 1000 * a1 + 100 * a2 + 10 * a3 + a4; // combine a1..a4
327    }
328
329    /**
330     * Extract the number of coils to process from RC/WC message.
331     *
332     * @param msg te SerialMessage received from Simulator inpipe
333     * @return the number of consecutive coils to read/write (decimal)
334     * after starting Coil
335     */
336    private int getNumberOfCoils(SerialMessage msg) {
337        int n1 = msg.getElement(9) - '0';  // N char 1
338        int n2 = msg.getElement(10) - '0'; // N char 2
339        return 10 * n1 + n2; // combine n1, n2
340    }
341
342    /**
343     * Write reply to output.
344     * <p>
345     * Adapted from jmri.jmrix.nce.simulator.SimulatorAdapter.
346     *
347     * @param r reply on message
348     */
349    private void writeReply(SerialReply r) {
350        if (r == null) {
351            return; // there is no reply to be sent
352        }
353        for (int i = 0; i < r.getNumDataElements(); i++) {
354            try {
355                outpipe.writeByte((byte) r.getElement(i));
356            } catch (java.io.IOException ex) {
357            }
358        }
359        try {
360            outpipe.flush();
361        } catch (java.io.IOException ex) {
362        }
363    }
364
365    /**
366     * Get characters from the input source.
367     * <p>
368     * Only used in the Receive thread.
369     *
370     * @return filled message, only when the message is complete
371     * @throws IOException when presented by the input source
372     */
373    private SerialMessage loadChars() throws java.io.IOException {
374        SerialReply reply = new SerialReply();
375        ((MapleSystemConnectionMemo) getSystemConnectionMemo()).getTrafficController().loadChars(reply, inpipe);
376
377        // copy received "reply" to a Maple message of known length
378        SerialMessage msg = new SerialMessage(reply.getNumDataElements());
379            for (int i = 0; i < msg.getNumDataElements(); i++) {
380                //log.debug("" + reply.getElement(i));
381                msg.setElement(i, reply.getElement(i));
382            }
383        log.debug("new message received");
384        return msg;
385    }
386
387    /**
388     * Set checksum on simulated Maple Node reply.
389     * Code copied from {@link SerialMessage}#setChecksum(int)
390     *
391     * @param r the SerialReply to complete
392     * @param index element index to place 2 checksum bytes
393     * @return SerialReply with parity set
394     */
395    public SerialReply setChecksum(SerialReply r, int index) {
396        int sum = 0;
397        for (int i = 1; i < index; i++) {
398            sum += r.getElement(i);
399        }
400        sum = sum & 0xFF;
401
402        char firstChar;
403        int firstVal = (sum / 16) & 0xF;
404        if (firstVal > 9) {
405            firstChar = (char) ('A' - 10 + firstVal);
406        } else {
407            firstChar = (char) ('0' + firstVal);
408        }
409        r.setElement(index, firstChar);
410
411        char secondChar;
412        int secondVal = sum & 0xf;
413        if (secondVal > 9) {
414            secondChar = (char) ('A' - 10 + secondVal);
415        } else {
416            secondChar = (char) ('0' + secondVal);
417        }
418        r.setElement(index + 1, secondChar);
419        return r;
420    }
421
422    int signalBankSize = 16; // theoretically: 16
423    int sensorBankSize = 64; // theoretically: 0x3F
424    javax.swing.Timer timer;
425
426    // streams to share with user class
427    private DataOutputStream pout = null; // this is provided to classes who want to write to us
428    private DataInputStream pin = null; // this is provided to classes who want data from us
429    // internal ends of the pipes
430    private DataOutputStream outpipe = null; // feed pin
431    private DataInputStream inpipe = null; // feed pout
432
433    private final static Logger log = LoggerFactory.getLogger(SimulatorAdapter.class);
434
435}