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