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 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 MapleSystemConnectionMemo("K", Bundle.getMessage("MapleSimulatorName"))); // pass customized user name
048        setManufacturer(jmri.jmrix.maple.SerialConnectionTypeList.MAPLE);
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 a MapleSimulator
103     * connected to this port.
104     */
105    @Override
106    public void configure() {
107        log.debug("set tc for memo {}", getSystemConnectionMemo().getUserName());
108        // connect to the traffic controller
109        ((MapleSystemConnectionMemo) getSystemConnectionMemo()).getTrafficController().connectPort(this);
110        // do the common manager config
111        ((MapleSystemConnectionMemo) getSystemConnectionMemo()).configureManagers();
112
113        // start the simulator
114        sourceThread = new Thread(this);
115        sourceThread.setName("Maple 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 Maple 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 Maple command station simulation.
198        log.info("Maple 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("Maple 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
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("Maple 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");
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.maple.SerialMessage}.
259     *
260     * @param msg the message received in the simulated node
261     * @return a single Maple 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        log.debug("Generate Reply to message from node {} (string = {})", msg.getAddress(), msg.toString());
266
267        SerialReply reply = new SerialReply(); // reply length is determined by highest byte added
268        int nodeAddress = msg.getUA();         // node addres from element 1 + 2
269        //convert hex to character
270        char cmd1 = (char) msg.getElement(3);  // command char 1
271        char cmd2 = (char) msg.getElement(4);  // command char 2
272
273        log.debug("Message nodeaddress={} cmd={}{}, Start={}, Num={}",
274                nodeAddress, cmd1, cmd2,
275                getStartAddress(msg), getNumberOfCoils(msg));
276
277        switch ("" + cmd1 + cmd2) {
278            case "RC": // Read Coils message
279                log.debug("Read Coils (poll) message detected");
280                int i = 1;
281                // init reply
282                log.debug("RC Reply from node {}", nodeAddress);
283                reply.setElement(0, 0x02); // <STX>
284                reply.setElement(1, msg.getElement(1));
285                reply.setElement(2, msg.getElement(2));
286                reply.setElement(3, 'R');
287                reply.setElement(4, 'C');
288                for (i = 1; i < getNumberOfCoils(msg); i++) {
289                    reply.setElement(i + 4, 0x00); // report state of each requested coil as Inactive = 0
290                    // TODO: echo commanded state from JMRI node-bit using: getCommandedState(nodeAddress * 1000 + getStartAddress(msg) + 1)
291                }
292                reply.setElement(i + 5, 0x03);
293                reply = setChecksum(reply, i + 6);
294                break;
295            case "WC": // Write Coils message
296                log.debug("Write Coils message detected");
297                // init reply
298                log.debug("WC Reply from node {}", nodeAddress);
299                reply.setElement(0, 0x06); // <ACK>
300                reply.setElement(1, msg.getElement(1));
301                reply.setElement(2, msg.getElement(2));
302                reply.setElement(3, 'W');
303                reply.setElement(4, 'C');
304                break;
305            default:
306                // TODO "WC" message replies
307                log.debug("command ignored");
308                reply = null; // ignore all other messages
309        }
310        log.debug("Reply {}", reply == null ? "empty, Message ignored" : "generated " + reply.toString());
311        return (reply);
312    }
313
314    /**
315     * Extract start coils from RC/WC message.
316     *
317     * @param msg te SerialMessage received from Simulator inpipe
318     * @return decimal coil ID
319     */
320    private int getStartAddress(SerialMessage msg) {
321        int a1 = msg.getElement(5) - '0';  // StartAt char 1
322        int a2 = msg.getElement(6) - '0';  // StartAt char 2
323        int a3 = msg.getElement(7) - '0';  // StartAt char 3
324        int a4 = msg.getElement(8) - '0';  // StartAt char 4
325        return 1000 * a1 + 100 * a2 + 10 * a3 + a4; // combine a1..a4
326    }
327
328    /**
329     * Extract the number of coils to process from RC/WC message.
330     *
331     * @param msg te SerialMessage received from Simulator inpipe
332     * @return the number of consecutive coils to read/write (decimal)
333     * after starting Coil
334     */
335    private int getNumberOfCoils(SerialMessage msg) {
336        int n1 = msg.getElement(9) - '0';  // N char 1
337        int n2 = msg.getElement(10) - '0'; // N char 2
338        return 10 * n1 + n2; // combine n1, n2
339    }
340
341    /**
342     * Write reply to output.
343     * <p>
344     * Adapted from jmri.jmrix.nce.simulator.SimulatorAdapter.
345     *
346     * @param r reply on message
347     */
348    private void writeReply(SerialReply r) {
349        if (r == null) {
350            return; // there is no reply to be sent
351        }
352        for (int i = 0; i < r.getNumDataElements(); i++) {
353            try {
354                outpipe.writeByte((byte) r.getElement(i));
355            } catch (java.io.IOException ex) {
356            }
357        }
358        try {
359            outpipe.flush();
360        } catch (java.io.IOException ex) {
361        }
362    }
363
364    /**
365     * Get characters from the input source.
366     * <p>
367     * Only used in the Receive thread.
368     *
369     * @return filled message, only when the message is complete
370     * @throws IOException when presented by the input source
371     */
372    private SerialMessage loadChars() throws java.io.IOException {
373        SerialReply reply = new SerialReply();
374        ((MapleSystemConnectionMemo) getSystemConnectionMemo()).getTrafficController().loadChars(reply, inpipe);
375
376        // copy received "reply" to a Maple message of known length
377        SerialMessage msg = new SerialMessage(reply.getNumDataElements());
378            for (int i = 0; i < msg.getNumDataElements(); i++) {
379                //log.debug("" + reply.getElement(i));
380                msg.setElement(i, reply.getElement(i));
381            }
382        log.debug("new message received");
383        return msg;
384    }
385
386    /**
387     * Set checksum on simulated Maple Node reply.
388     * Code copied from {@link SerialMessage}#setChecksum(int)
389     *
390     * @param r the SerialReply to complete
391     * @param index element index to place 2 checksum bytes
392     * @return SerialReply with parity set
393     */
394    public SerialReply setChecksum(SerialReply r, int index) {
395        int sum = 0;
396        for (int i = 1; i < index; i++) {
397            sum += r.getElement(i);
398        }
399        sum = sum & 0xFF;
400
401        char firstChar;
402        int firstVal = (sum / 16) & 0xF;
403        if (firstVal > 9) {
404            firstChar = (char) ('A' - 10 + firstVal);
405        } else {
406            firstChar = (char) ('0' + firstVal);
407        }
408        r.setElement(index, firstChar);
409
410        char secondChar;
411        int secondVal = sum & 0xf;
412        if (secondVal > 9) {
413            secondChar = (char) ('A' - 10 + secondVal);
414        } else {
415            secondChar = (char) ('0' + secondVal);
416        }
417        r.setElement(index + 1, secondChar);
418        return r;
419    }
420
421    int signalBankSize = 16; // theoretically: 16
422    int sensorBankSize = 64; // theoretically: 0x3F
423    javax.swing.Timer timer;
424
425    // streams to share with user class
426    private DataOutputStream pout = null; // this is provided to classes who want to write to us
427    private DataInputStream pin = null; // this is provided to classes who want data from us
428    // internal ends of the pipes
429    private DataOutputStream outpipe = null; // feed pin
430    private DataInputStream inpipe = null; // feed pout
431
432    private final static Logger log = LoggerFactory.getLogger(SimulatorAdapter.class);
433
434}