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