001package jmri.jmrix.grapevine.simulator;
002
003import java.io.*;
004
005import javax.annotation.Nonnull;
006
007// no special xSimulatorController
008import jmri.jmrix.grapevine.*;
009import jmri.util.ImmediatePipedOutputStream;
010import jmri.util.swing.JmriJOptionPane;
011
012/**
013 * Provide access to a simulated Grapevine system.
014 * <p>
015 * Currently, the Grapevine SimulatorAdapter reacts to the following commands sent from the user
016 * interface with an appropriate reply {@link #generateReply(SerialMessage)}:
017 * <ul>
018 *     <li>Software version (poll)
019 *     <li>Renumber (displays dialog: not supported)
020 *     <li>Node Init (2 replies + user configurable node-bank-bit status)
021 *     <li>Set signal/sensor/turnout (echoes message)
022 * </ul>
023 *
024 * Based on jmri.jmrix.lenz.xnetsimulator.XNetSimulatorAdapter / EasyDCCSimulatorAdapter 2017
025 * <p>
026 * NOTE: Some material in this file was modified from other portions of the
027 * support infrastructure.
028 *
029 * @author Paul Bender, Copyright (C) 2009-2010
030 * @author Mark Underwood, Copyright (C) 2015
031 * @author Egbert Broerse, Copyright (C) 2018
032 */
033@SuppressWarnings("javadoc")
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     * Simulator auto-init setting for number of banks to auto-reply on poll
043     */
044    private int autoInit = 0;
045
046    /**
047     * Create a new SimulatorAdapter.
048     */
049    public SimulatorAdapter() {
050        super(new GrapevineSystemConnectionMemo("G", Bundle.getMessage("GrapevineSimulatorName"))); // pass customized user name
051        option1Name = "InitPreference"; // NOI18N
052        // init pref setting, the default is No init
053        options.put(option1Name, new Option(Bundle.getMessage("AutoInitLabel"),
054                new String[]{Bundle.getMessage("ButtonNoInit"),
055                Bundle.getMessage("ButtonAll"), Bundle.getMessage("Button4Each")}));
056        setManufacturer(jmri.jmrix.grapevine.SerialConnectionTypeList.PROTRAK);
057    }
058
059    /**
060     * {@inheritDoc}
061     * Simulated input/output pipes.
062     */
063    @Override
064    public String openPort(String portName, String appName) {
065        try {
066            PipedOutputStream tempPipeI = new ImmediatePipedOutputStream();
067            log.debug("tempPipeI created");
068            pout = new DataOutputStream(tempPipeI);
069            inpipe = new DataInputStream(new PipedInputStream(tempPipeI));
070            PipedOutputStream tempPipeO = new ImmediatePipedOutputStream();
071            outpipe = new DataOutputStream(tempPipeO);
072            pin = new DataInputStream(new PipedInputStream(tempPipeO));
073        } catch (java.io.IOException e) {
074            log.error("init (pipe): Exception: {}", e.toString());
075        }
076        opened = true;
077        return null; // indicates OK return
078    }
079
080    /**
081     * Set if the output buffer is empty or full. This should only be set to
082     * false by external processes.
083     *
084     * @param s true if output buffer is empty; false otherwise
085     */
086    synchronized public void setOutputBufferEmpty(boolean s) {
087        outputBufferEmpty = s;
088    }
089
090    /**
091     * Can the port accept additional characters? The state of CTS determines
092     * this, as there seems to be no way to check the number of queued bytes and
093     * buffer length. This might go false for short intervals, but it might also
094     * stick off if something goes wrong.
095     *
096     * @return true if port can accept additional characters; false otherwise
097     */
098    public boolean okToSend() {
099        if (checkBuffer) {
100            log.debug("Buffer Empty: {}", outputBufferEmpty);
101            return (outputBufferEmpty);
102        } else {
103            log.debug("No Flow Control or Buffer Check");
104            return (true);
105        }
106    }
107
108    /**
109     * Set up all of the other objects to operate with a GrapevineSimulator
110     * connected to this port.
111     */
112    @Override
113    public void configure() {
114        // connect to the traffic controller
115        log.debug("set tc for memo {}", getSystemConnectionMemo().getUserName());
116        SerialTrafficController control = new SerialTrafficController(getSystemConnectionMemo());
117        //compare with: XNetTrafficController packets = new XNetPacketizer(new LenzCommandStation());
118        control.connectPort(this);
119        getSystemConnectionMemo().setTrafficController(control);
120        // do the common manager config
121        getSystemConnectionMemo().configureManagers();
122
123        if (getOptionState(option1Name).equals(getOptionChoices(option1Name)[1])) {
124            autoInit = 1; // auto-init all bits
125        } else if (getOptionState(option1Name).equals(getOptionChoices(option1Name)[2])) {
126            autoInit = 2; // first 4 items
127        }   // default = none, also after locale change just to be safe
128
129        // start the simulator
130        sourceThread = new Thread(this);
131        sourceThread.setName("Grapevine Simulator");
132        sourceThread.setPriority(Thread.MIN_PRIORITY);
133        sourceThread.start();
134    }
135
136    /**
137     * {@inheritDoc}
138     */
139    @Override
140    public void connect() throws java.io.IOException {
141        log.debug("connect called");
142        super.connect();
143    }
144
145    // Base class methods for the Grapevine SerialPortController simulated interface
146
147    /**
148     * {@inheritDoc}
149     */
150    @Override
151    public DataInputStream getInputStream() {
152        if (!opened || pin == null) {
153            log.error("getInputStream called before load(), stream not available");
154        }
155        log.debug("DataInputStream pin returned");
156        return pin;
157    }
158
159    /**
160     * {@inheritDoc}
161     */
162    @Override
163    public DataOutputStream getOutputStream() {
164        if (!opened || pout == null) {
165            log.error("getOutputStream called before load(), stream not available");
166        }
167        log.debug("DataOutputStream pout returned");
168        return pout;
169    }
170
171    /**
172     * {@inheritDoc}
173     * @return always true, given this SimulatorAdapter is running
174     */
175    @Override
176    public boolean status() {
177        return opened;
178    }
179
180    /**
181     * {@inheritDoc}
182     *
183     * @return null
184     */
185    @Override
186    public String[] validBaudRates() {
187        log.debug("validBaudRates should not have been invoked");
188        return new String[]{};
189    }
190
191    /**
192     * {@inheritDoc}
193     */
194    @Override
195    public int[] validBaudNumbers() {
196        return new int[]{};
197    }
198
199    @Override
200    public String getCurrentBaudRate() {
201        return "";
202    }
203
204    @Override
205    public String getCurrentPortName(){
206        return "";
207    }
208
209    @Override
210    public void run() { // start a new thread
211        // This thread has one task. It repeatedly reads from the input pipe
212        // and writes an appropriate response to the output pipe. This is the heart
213        // of the Grapevine command station simulation.
214        log.info("Grapevine Simulator Started");
215        while (true) {
216            try {
217                synchronized (this) {
218                    wait(50);
219                }
220            } catch (InterruptedException e) {
221                log.debug("interrupted, ending");
222                return;
223            }
224            SerialMessage m = readMessage();
225            SerialReply r;
226            if (log.isDebugEnabled()) {
227                StringBuffer buf = new StringBuffer();
228                if (m != null) {
229                    for (int i = 0; i < m.getNumDataElements(); i++) {
230                        buf.append(Integer.toHexString(0xFF & m.getElement(i))).append(" ");
231                    }
232                } else {
233                    buf.append("null message buffer");
234                }
235                log.trace("Grapevine Simulator Thread received message: {}", buf); // generates a lot of traffic
236            }
237            if (m != null) {
238                r = generateReply(m);
239                if (r != null) { // ignore errors
240                    writeReply(r);
241                    if (log.isDebugEnabled()) {
242                        StringBuilder buf = new StringBuilder();
243                        for (int i = 0; i < r.getNumDataElements(); i++) {
244                            buf.append(Integer.toHexString(0xFF & r.getElement(i))).append(" ");
245                        }
246                        log.debug("Grapevine Simulator Thread sent reply: {}", buf );
247                    }
248                }
249            }
250        }
251    }
252
253    /**
254     * Read one incoming message from the buffer
255     * and set outputBufferEmpty to true.
256     */
257    private SerialMessage readMessage() {
258        SerialMessage msg = null;
259        // log.debug("Simulator reading message");
260        try {
261            if (inpipe != null && inpipe.available() > 0) {
262                msg = loadChars();
263            }
264        } catch (java.io.IOException e) {
265            // should do something meaningful here.
266        }
267        setOutputBufferEmpty(true);
268        return (msg);
269    }
270
271    /**
272     * This is the heart of the simulation. It translates an
273     * incoming SerialMessage into an outgoing SerialReply.
274     * See {@link jmri.jmrix.grapevine.SerialMessage}#generateReply(SerialMessage) and
275     * the Grapevine <a href="../package-summary.html">Binary Message Format Summary</a>.
276     *
277     * @param msg the message received in the simulated node
278     * @return a single Grapevine message to confirm the requested operation, or a series
279     * of messages for each (fictitious) node/pin/state. To ignore certain commands, return null.
280     */
281    private SerialReply generateReply(SerialMessage msg) {
282        log.debug("Generate Reply to message from node {} (string = {})", msg.getAddr(), msg.toString());
283
284        SerialReply reply = new SerialReply(); // reply length is determined by highest byte added
285        int nodeaddr = msg.getAddr();          // node addres from element(0)
286        int b1 = msg.getElement(0);            // raw hex value from element(0)
287        int b2 = msg.getElement(1);            // bit + state
288        int b3 = msg.getElement(2);            // element(2), must repeat node address
289        int b4 = msg.getElement(3);            // bank + parity
290        int bank = (b4 & 0xF0) >> 4;           // bank # on node, 0 on node initialization
291        log.debug("Message nodeaddress={} b1={} b2={} b3={} b4={}", nodeaddr, b1, b2, b3, b4);
292
293        if (nodeaddr == 0) { // error
294            log.debug("general error: coded as: {}", (((b4 & 0x70) << 4) - 1));
295            return null;
296        }
297
298        switch (b2) {
299
300            case 119:
301                log.debug("get software version (poll) message detected");
302                // 2 byte software version number reply
303                reply.setElement(0, nodeaddr | 0x80);
304                reply.setElement(1, 9); // pretend version "9"
305                // no parity
306                break;
307
308            case 0x71 :
309                log.debug("init node message 1 detected - ASD sensors");
310                // init reply as set in prefs autoInit
311                if (autoInit > 0) { // not disabled
312                    log.debug("start init 1 of node {}", nodeaddr);
313                    nodeResponse(nodeaddr, 1, 1, autoInit); // banks 1-4
314                }
315                // all replies are generated and sent by nodeResponse()
316                reply = null;
317                break;
318
319            case 0x73: //(b2 == 0x70) && ((b4 & 0xF0) == 0x10)
320                log.debug("init node message 2 detected - parallel sensors");
321                // init reply as set in prefs autoInit
322                if (autoInit > 0) { // not disabled
323                    log.debug("start init 2 of node {}", nodeaddr);
324                    nodeResponse(nodeaddr, 5, 5, autoInit); // bank 5 = parallel
325                }
326                // all replies are generated and sent by nodeResponse()
327                reply = null;
328                break;
329
330            default:
331                if (bank == 0x6) { // this is the rename command, with element 2 = new node number
332                    JmriJOptionPane.showMessageDialog(null,
333                            Bundle.getMessage("RenumberSupport"),
334                            Bundle.getMessage("MessageTitle"),
335                            JmriJOptionPane.ERROR_MESSAGE);
336                    log.debug("rename command not supported, old address: {}, new address: {}, bank: {}",
337                            nodeaddr, b2, bank);
338                } else {
339                    log.debug("echo normal command, node {} bank {} ignored", nodeaddr, bank);
340                    reply = null; // ignore all other messages
341                    // alternatavely, send a 4 byte general reply:
342                    // reply.setElement(0, (nodeaddr | 0x80));
343                    // reply.setElement(1, (b2 & 0xFF));  // normally: bit + state
344                    // reply.setElement(2, (nodeaddr | 0x80));
345                    // reply.setElement(3, (bank << 4)); // 0 = error, bank 1..3 for signals, 4..5 sensors (and parity)
346                    // reply = setParity(reply, 0);
347                }
348        }
349        log.debug("Reply {}", reply == null ? "empty, Message ignored" : " generated " + reply.toString());
350        return reply;
351    }
352
353    /**
354     * Write reply to output.
355     * <p>
356     * Adapted from jmri.jmrix.nce.simulator.SimulatorAdapter.
357     *
358     * @param r reply on message
359     */
360    private void writeReply(@Nonnull SerialReply r) {
361        for (int i = 0; i < r.getNumDataElements(); i++) {
362            try {
363                outpipe.writeByte((byte) r.getElement(i));
364            } catch (java.io.IOException ignored) {
365            }
366        }
367        try {
368            outpipe.flush();
369        } catch (java.io.IOException ignored) {
370        }
371    }
372
373    /**
374     * Get characters from the input source.
375     * <p>
376     * Only used in the Receive thread.
377     *
378     * @return filled message, only when the message is complete.
379     * @throws IOException when presented by the input source.
380     */
381    private SerialMessage loadChars() throws java.io.IOException {
382        int nchars;
383        byte[] rcvBuffer = new byte[32];
384
385        nchars = inpipe.read(rcvBuffer, 0, 32);
386        //log.debug("new message received");
387        SerialMessage msg = new SerialMessage(nchars);
388
389        for (int i = 0; i < nchars; i++) {
390            msg.setElement(i, rcvBuffer[i] & 0xFF);
391        }
392        return msg;
393    }
394
395    /**
396     * Set parity on simulated Grapevine Node reply.
397     * Code copied from {@link SerialMessage#setParity(int)}
398     *
399     * @param r the SerialReply to complete
400     * @param start bit index to start
401     * @return SerialReply with parity set
402     */
403    public SerialReply setParity(SerialReply r, int start) {
404        // nibble sum method
405        int sum = r.getElement(0 + start) & 0x0F;
406        sum += (r.getElement(0 + start) & 0x70) >> 4;
407        sum += (r.getElement(1 + start) * 2) & 0x0F;
408        sum += ((r.getElement(1 + start) * 2) & 0xF0) >> 4;
409        sum += (r.getElement(3 + start) & 0x70) >> 4;
410        //log.debug("Parity element read: {}",
411        //       Integer.toHexString(r.getElement(3 + start) & 0x70));
412        int parity = 16 - (sum & 0xF);
413
414        r.setElement(3 + start, (r.getElement(3 + start) & 0xF0) | (parity & 0xF));
415        return r;
416    }
417
418    int signalBankSize = 16; // theoretically: 16
419    int sensorBankSize = 64; // theoretically: 0x3F
420    javax.swing.Timer timer;
421
422    /**
423     * Pretend a node init reply for a range of banks and bits. Is this a proper simulation of hardware?
424     * <p>
425     * Based on information in jmri.jmrix.grapevine.SerialMessage#staticFormat(int, int, int, int).
426     *
427     * @param node      the node address
428     * @param startBank first bank id to report
429     * @param endBank   last bank id to report
430     * @param initBits  number of inputs/output bits to report
431     */
432    private void nodeResponse(int node, int startBank, int endBank, int initBits) {
433        if (node < 1 || node > 127) { // node address invalid
434            log.warn("Invalid Node Address; no response generated");
435            return;
436        }
437        if (initBits > 1) { // leave at max when 1
438            signalBankSize = 4; // only first 4 signal bits reporting
439            sensorBankSize = 4; // only first 4 sensor bits reporting
440        }
441        int b1 = -1;
442        int b2 = -1;
443        int b3 = -1;
444        int b4 = -1;
445
446        SerialReply nReply = new SerialReply(); // reply length is determined by highest byte added
447        nReply.setElement(0, node | 0x80);
448        nReply.setElement(2, node | 0x80);
449
450        for (int k = startBank; k <= endBank; k++) { // bank
451            if (k <= 3) { // bank 1 to 3, signals
452                nReply.setElement(3, (k << 4)); // bank (bit 1234): 1-3 = signals
453                log.debug("element 3 set to 0x{} - {}", (k << 4) & 0x70, Integer.toBinaryString((k << 4) & 0x70));
454
455                for (int j = 1; j < signalBankSize; j++) { // bits, send state of each signal bit (banks 1, 2, 3)
456                    log.debug("Sending signal state of node {}, bank {}, bit {}", node, k, j);
457                    nReply.setElement(1, ((j << 3) | 0x6) & 0x7F); // bit id (bits 2345) + state (bits 678): set to Red
458
459                    nReply = setParity(nReply, 0);
460                    writeReply(nReply);
461                    // check
462                    b1 = nReply.getElement(0) & 0x7F;  // raw hex value from element(0)
463                    b2 = nReply.getElement(1) & 0x7F;  // bit + state
464                    b3 = nReply.getElement(2) & 0x7F;  // element(2), repeat node address
465                    b4 = nReply.getElement(3) & 0xFF;  // bank + parity
466                    if (b1 != b3) {
467                        log.error("Address mismatch on node {} bank {} bit {}", node, k, j);
468                    }
469                    log.debug("Reply written for node {} bank {} bit {}: b1= {} b2={} b3={} b4={}", node, k, j, b1, b2, b3, b4);
470                    log.debug("Reply as hex: {} {} {} {}", Integer.toHexString(b1),
471                            Integer.toHexString(b2), Integer.toHexString(b3), Integer.toHexString(b4));
472                    log.debug("Reply as bin: {} - {} - {} - {}", Integer.toBinaryString(b1),
473                            Integer.toBinaryString(b2), Integer.toBinaryString(b3), Integer.toBinaryString(b4));
474                }
475            } else { // bank 4 and 5, sensors
476                nReply.setElement(3, (k << 4)); // bank (bit 1234): 4-5 = sensors
477                log.debug("element 3 set to 0x{} - {}", (k << 4) & 0x70, Integer.toBinaryString((k << 4) & 0x70));
478
479                for (int j = 1; j < sensorBankSize; j++) { // bits, send state of each sensor bit (banks 4, 5)
480                    log.debug("Sending sensor state of node {}, bank {}, bit {}", node, k, j);
481                    nReply.setElement(1, ((j << 1) | 0x1) & 0x7F); // bit id (bits 234567) + state (bit 8): inactive
482
483                    nReply = setParity(nReply,0);
484                    writeReply(nReply);
485                    // check
486                    b1 = nReply.getElement(0) & 0x7F;  // raw hex value from element(0)
487                    b2 = nReply.getElement(1) & 0x7F;  // bit + state
488                    b3 = nReply.getElement(2) & 0x7F;  // element(2), repeat node address
489                    b4 = nReply.getElement(3) & 0xFF;  // bank + parity
490                    if (b1 != b3) {
491                        log.error("Address mismatch on node {} bank {} bit {}", node, k, j);
492                    }
493                    log.debug("Reply written for node {} bank {} bit {}: b1= {} b2={} b3={} b4={}", node, k, j, b1, b2, b3, b4);
494                    log.debug("Reply as hex: {} {} {} {}", Integer.toHexString(b1),
495                            Integer.toHexString(b2), Integer.toHexString(b3), Integer.toHexString(b4));
496                    log.debug("Reply as bin: {} - {} - {} - {}", Integer.toBinaryString(b1),
497                            Integer.toBinaryString(b2), Integer.toBinaryString(b3), Integer.toBinaryString(b4));               }
498            }
499        }
500    }
501
502    // streams to share with user class
503    private DataOutputStream pout = null; // this is provided to classes who want to write to us
504    private DataInputStream pin = null; // this is provided to classes who want data from us
505    // internal ends of the pipes
506    private DataOutputStream outpipe = null; // feed pin
507    private DataInputStream inpipe = null; // feed pout
508
509    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(SimulatorAdapter.class);
510
511}