001package jmri.jmrix.dccpp.simulator;
002
003import java.io.DataInputStream;
004import java.io.DataOutputStream;
005import java.io.IOException;
006import java.io.PipedInputStream;
007import java.io.PipedOutputStream;
008import java.time.LocalDateTime;
009import java.time.format.DateTimeFormatter;
010import java.util.LinkedHashMap;
011import java.util.concurrent.ThreadLocalRandom;
012import java.util.regex.Matcher;
013import java.util.regex.Pattern;
014import java.util.regex.PatternSyntaxException;
015import jmri.jmrix.ConnectionStatus;
016import jmri.jmrix.dccpp.DCCppCommandStation;
017import jmri.jmrix.dccpp.DCCppConstants;
018import jmri.jmrix.dccpp.DCCppInitializationManager;
019import jmri.jmrix.dccpp.DCCppMessage;
020import jmri.jmrix.dccpp.DCCppPacketizer;
021import jmri.jmrix.dccpp.DCCppReply;
022import jmri.jmrix.dccpp.DCCppSimulatorPortController;
023import jmri.jmrix.dccpp.DCCppTrafficController;
024import jmri.util.ImmediatePipedOutputStream;
025import org.slf4j.Logger;
026import org.slf4j.LoggerFactory;
027
028import edu.umd.cs.findbugs.annotations.*;
029
030/**
031 * Provide access to a simulated DCC-EX system.
032 *
033 * Currently, the DCCppSimulator reacts to commands sent from the user interface
034 * with messages an appropriate reply message.
035 *
036 * NOTE: Most DCC-EX commands are still unsupported in this implementation.
037 *
038 * Normally controlled by the dccpp.DCCppSimulator.DCCppSimulatorFrame class.
039 *
040 * NOTE: Some material in this file was modified from other portions of the
041 * support infrastructure.
042 *
043 * @author Paul Bender, Copyright (C) 2009-2010
044 * @author Mark Underwood, Copyright (C) 2015
045 * @author M Steve Todd, 2021
046 *
047 * Based on {@link jmri.jmrix.lenz.xnetsimulator.XNetSimulatorAdapter}
048 */
049public class DCCppSimulatorAdapter extends DCCppSimulatorPortController implements Runnable {
050
051    static final int SENSOR_MSG_RATE = 10;
052
053    private boolean outputBufferEmpty = true;
054    private final boolean checkBuffer = true;
055    private boolean trackPowerState = false;
056    // One extra array element so that i can index directly from the
057    // CV value, ignoring CVs[0].
058    private final int[] CVs = new int[DCCppConstants.MAX_DIRECT_CV + 1];
059
060    private java.util.TimerTask keepAliveTimer; // Timer used to periodically
061    private static final long keepAliveTimeoutValue = 30000; // Interval
062    //keep track of recreation command, including state, for each turnout and output
063    private LinkedHashMap<Integer,String> turnouts = new LinkedHashMap<Integer, String>();
064    //keep track of speed, direction and functions for each loco address
065    private LinkedHashMap<Integer,Integer> locoSpeedByte = new LinkedHashMap<Integer,Integer>();
066    private LinkedHashMap<Integer,Integer> locoFunctions = new LinkedHashMap<Integer,Integer>();
067
068    public DCCppSimulatorAdapter() {
069        setPort(Bundle.getMessage("None"));
070        try {
071            PipedOutputStream tempPipeI = new ImmediatePipedOutputStream();
072            pout = new DataOutputStream(tempPipeI);
073            inpipe = new DataInputStream(new PipedInputStream(tempPipeI));
074            PipedOutputStream tempPipeO = new ImmediatePipedOutputStream();
075            outpipe = new DataOutputStream(tempPipeO);
076            pin = new DataInputStream(new PipedInputStream(tempPipeO));
077        } catch (java.io.IOException e) {
078            log.error("init (pipe): Exception: {}", e.toString());
079            return;
080        }
081        // Zero out the CV table.
082        for (int i = 0; i < DCCppConstants.MAX_DIRECT_CV + 1; i++) {
083            CVs[i] = 0;
084        }
085    }
086
087    @Override
088    public String openPort(String portName, String appName) {
089        // open the port in XpressNet mode, check ability to set moderators
090        setPort(portName);
091        return null; // normal operation
092    }
093
094    /**
095     * Set if the output buffer is empty or full. This should only be set to
096     * false by external processes.
097     *
098     * @param s true if output buffer is empty; false otherwise
099     */
100    @Override
101    synchronized public void setOutputBufferEmpty(boolean s) {
102        outputBufferEmpty = s;
103    }
104
105    /**
106     * Can the port accept additional characters? The state of CTS determines
107     * this, as there seems to be no way to check the number of queued bytes and
108     * buffer length. This might go false for short intervals, but it might also
109     * stick off if something goes wrong.
110     *
111     * @return true if port can accept additional characters; false otherwise
112     */
113    @Override
114    public boolean okToSend() {
115        if (checkBuffer) {
116            log.debug("Buffer Empty: {}", outputBufferEmpty);
117            return (outputBufferEmpty);
118        } else {
119            log.debug("No Flow Control or Buffer Check");
120            return (true);
121        }
122    }
123
124    /**
125     * Set up all of the other objects to operate with a DCCppSimulator
126     * connected to this port
127     */
128    @Override
129    public void configure() {
130        // connect to a packetizing traffic controller
131        DCCppTrafficController packets = new DCCppPacketizer(new DCCppCommandStation());
132        packets.connectPort(this);
133
134        // start operation
135        // packets.startThreads();
136        this.getSystemConnectionMemo().setDCCppTrafficController(packets);
137
138        sourceThread = jmri.util.ThreadingUtil.newThread(this);
139        sourceThread.start();
140
141        new DCCppInitializationManager(this.getSystemConnectionMemo());
142    }
143
144    /**
145     * Set up the keepAliveTimer, and start it.
146     */
147    private void keepAliveTimer() {
148        if (keepAliveTimer == null) {
149            keepAliveTimer = new java.util.TimerTask(){
150                @Override
151                public void run() {
152                    // When the timer times out, send a heartbeat (status request on DCC-EX, max num slots request on DCC-EX
153                    DCCppTrafficController tc = DCCppSimulatorAdapter.this.getSystemConnectionMemo().getDCCppTrafficController();
154                    DCCppCommandStation cs = tc.getCommandStation();
155                    if (cs.isMaxNumSlotsMsgSupported()) {
156                        tc.sendDCCppMessage(jmri.jmrix.dccpp.DCCppMessage.makeCSMaxNumSlotsMsg(), null);
157                    } else {
158                        tc.sendDCCppMessage(jmri.jmrix.dccpp.DCCppMessage.makeCSStatusMsg(), null);
159                    }
160
161                }
162            };
163        } else {
164            keepAliveTimer.cancel();
165        }
166        jmri.util.TimerUtil.schedule(keepAliveTimer, keepAliveTimeoutValue, keepAliveTimeoutValue);
167    }
168
169
170    // base class methods for the DCCppSimulatorPortController interface
171
172    /**
173     * {@inheritDoc}
174     */
175    @Override
176    public DataInputStream getInputStream() {
177        if (pin == null) {
178            log.error("getInputStream called before load(), stream not available");
179            ConnectionStatus.instance().setConnectionState(
180                    getSystemConnectionMemo(), ConnectionStatus.CONNECTION_DOWN);
181        }
182        return pin;
183    }
184
185    /**
186     * {@inheritDoc}
187     */
188    @Override
189    public DataOutputStream getOutputStream() {
190        if (pout == null) {
191            log.error("getOutputStream called before load(), stream not available");
192            ConnectionStatus.instance().setConnectionState(
193                    getSystemConnectionMemo(), ConnectionStatus.CONNECTION_DOWN);
194        }
195        return pout;
196    }
197
198    /**
199     * {@inheritDoc}
200     */
201    @Override
202    public boolean status() {
203        return (pout != null && pin != null);
204    }
205
206    /**
207     * {@inheritDoc}
208     * Currently just a message saying it's fixed.
209     *
210     * @return null
211     */
212    @Override
213    public String[] validBaudRates() {
214        return new String[]{};
215    }
216
217    /**
218     * {@inheritDoc}
219     */
220    @Override
221    public int[] validBaudNumbers() {
222        return new int[]{};
223    }
224
225    @Override
226    public void run() { // start a new thread
227        // this thread has one task.  It repeatedly reads from the input pipe
228        // and writes modified data to the output pipe.  This is the heart
229        // of the command station simulation.
230        log.debug("Simulator Thread Started");
231
232        keepAliveTimer();
233
234        ConnectionStatus.instance().setConnectionState(
235                getSystemConnectionMemo(), ConnectionStatus.CONNECTION_UP);
236        for (;;) {
237            DCCppMessage m = readMessage();
238            log.debug("Simulator Thread received message '{}'", m);
239            DCCppReply r = generateReply(m);
240            // If generateReply() returns null, do nothing. No reply to send.
241            if (r != null) {
242                writeReply(r);
243            }
244
245            // Once every SENSOR_MSG_RATE loops, generate a random Sensor message.
246            int rand = ThreadLocalRandom.current().nextInt(SENSOR_MSG_RATE);
247            if (rand == 1) {
248                generateRandomSensorReply();
249            }
250        }
251    }
252
253    // readMessage reads one incoming message from the buffer
254    // and sets outputBufferEmpty to true.
255    private DCCppMessage readMessage() {
256        DCCppMessage msg = null;
257        try {
258            msg = loadChars();
259        } catch (java.io.IOException e) {
260            // should do something meaningful here.
261            ConnectionStatus.instance().setConnectionState(
262                    getSystemConnectionMemo(), ConnectionStatus.CONNECTION_DOWN);
263
264        }
265        setOutputBufferEmpty(true);
266        return (msg);
267    }
268
269    // generateReply is the heart of the simulation.  It translates an
270    // incoming DCCppMessage into an outgoing DCCppReply.
271    @SuppressFBWarnings( value="FS_BAD_DATE_FORMAT_FLAG_COMBO", justification = "both am/pm and 24hr flags present ok as only used for display output")
272    private DCCppReply generateReply(DCCppMessage msg) {
273        String s, r = null;
274        Pattern p;
275        Matcher m;
276        DCCppReply reply = null;
277
278        log.debug("Generate Reply to message type '{}' string = '{}'", msg.getElement(0), msg);
279
280        switch (msg.getElement(0)) {
281
282            case DCCppConstants.THROTTLE_CMD:
283                log.debug("THROTTLE_CMD detected");
284                s = msg.toString();
285                try {
286                    p = Pattern.compile(DCCppConstants.THROTTLE_CMD_REGEX);
287                    m = p.matcher(s); //<t REG CAB SPEED DIR>
288                    if (!m.matches()) {
289                        p = Pattern.compile(DCCppConstants.THROTTLE_V3_CMD_REGEX);
290                        m = p.matcher(s); //<t locoId speed dir>
291                        if (!m.matches()) {
292                            log.error("Malformed Throttle Command: {}", s);
293                            return (null);
294                        }
295                        int locoId = Integer.parseInt(m.group(1));
296                        int speed = Integer.parseInt(m.group(2));
297                        int dir = Integer.parseInt(m.group(3));
298                        storeLocoSpeedByte(locoId, speed, dir);
299                        r = getLocoStateString(locoId);
300                    } else {
301                        r = "T " + m.group(1) + " " + m.group(3) + " " + m.group(4);
302                    }
303                } catch (PatternSyntaxException e) {
304                    log.error("Malformed pattern syntax! ");
305                    return (null);
306                } catch (IllegalStateException e) {
307                    log.error("Group called before match operation executed string= {}", s);
308                    return (null);
309                } catch (IndexOutOfBoundsException e) {
310                    log.error("Index out of bounds string= {}", s);
311                    return (null);
312                }
313                reply = DCCppReply.parseDCCppReply(r);
314                log.debug("Reply generated = '{}'", reply);
315                break;
316
317            case DCCppConstants.FUNCTION_V4_CMD:
318                log.debug("FunctionV4Detected");
319                s = msg.toString();
320                r = "";
321                try {
322                    p = Pattern.compile(DCCppConstants.FUNCTION_V4_CMD_REGEX);
323                    m = p.matcher(s); //<F locoId func 1|0>
324                    if (!m.matches()) {
325                        log.error("Malformed FunctionV4 Command: {}", s);
326                        return (null);
327                    }
328                    int locoId = Integer.parseInt(m.group(1));
329                    int fn = Integer.parseInt(m.group(2));
330                    int state = Integer.parseInt(m.group(3));
331                    storeLocoFunction(locoId, fn, state);
332                    r = getLocoStateString(locoId);
333                } catch (PatternSyntaxException e) {
334                    log.error("Malformed pattern syntax!");
335                    return (null);
336                } catch (IllegalStateException e) {
337                    log.error("Group called before match operation executed string= {}", s);
338                    return (null);
339                } catch (IndexOutOfBoundsException e) {
340                    log.error("Index out of bounds string= {}", s);
341                    return (null);
342                }
343                reply = DCCppReply.parseDCCppReply(r);
344                log.debug("Reply generated = '{}'", reply);
345                break;
346
347            case DCCppConstants.TURNOUT_CMD:
348                if (msg.isTurnoutAddMessage()
349                        || msg.isTurnoutAddDCCMessage()
350                        || msg.isTurnoutAddServoMessage()
351                        || msg.isTurnoutAddVpinMessage()) {
352                    log.debug("Add Turnout Message");
353                    s = "H" + msg.toString().substring(1) + " 0"; //T reply is H, init to closed
354                    turnouts.put(msg.getTOIDInt(), s);
355                    r = "O";
356                } else if (msg.isTurnoutDeleteMessage()) {
357                    log.debug("Delete Turnout Message");
358                    turnouts.remove(msg.getTOIDInt());
359                    r = "O";
360                } else if (msg.isListTurnoutsMessage()) {
361                    log.debug("List Turnouts Message");
362                    generateTurnoutListReply();
363                    break;
364                } else if (msg.isTurnoutCmdMessage()) {
365                    log.debug("Turnout Command Message");
366                    s = turnouts.get(msg.getTOIDInt()); //retrieve the stored turnout def
367                    if (s != null) {
368                        s = s.substring(0, s.length()-1) + msg.getTOStateInt(); //replace the last char with new state
369                        turnouts.put(msg.getTOIDInt(), s); //update the stored turnout
370                        r = "H " + msg.getTOIDString() + " " + msg.getTOStateInt();
371                    } else {
372                        log.warn("Unknown turnout ID '{}'", msg.getTOIDInt());
373                        r = "X";
374                    }
375
376                } else {
377                    log.debug("Unknown TURNOUT_CMD detected");
378                    r = "X";
379                }
380                reply = DCCppReply.parseDCCppReply(r);
381                log.debug("Reply generated = '{}'", reply);
382                break;
383
384            case DCCppConstants.OUTPUT_CMD:
385                if (msg.isOutputCmdMessage()) {
386                    log.debug("Output Command Message: '{}'", msg);
387                    s = turnouts.get(msg.getOutputIDInt()); //retrieve the stored turnout def
388                    if (s != null) {
389                        s = s.substring(0, s.length()-1) + (msg.getOutputStateBool() ? "1" : "0"); //replace the last char with new state
390                        turnouts.put(msg.getOutputIDInt(), s); //update the stored turnout
391                        r = "Y " + msg.getOutputIDInt() + " " + (msg.getOutputStateBool() ? "1" : "0");
392                        reply = DCCppReply.parseDCCppReply(r);
393                        log.debug("Reply generated = {}", reply.toString());
394                    } else {
395                        log.warn("Unknown output ID '{}'", msg.getOutputIDInt());
396                        r = "X";
397                    }
398                } else if (msg.isOutputAddMessage()) {
399                    log.debug("Output Add Message");
400                    s = "Y" + msg.toString().substring(1) + " 0"; //Z reply is Y, init to closed
401                    turnouts.put(msg.getOutputIDInt(), s);
402                    r = "O";
403                } else if (msg.isOutputDeleteMessage()) {
404                    log.debug("Output Delete Message");
405                    turnouts.remove(msg.getOutputIDInt());
406                    r = "O";
407                } else if (msg.isListOutputsMessage()) {
408                    log.debug("Output List Message");
409                    generateTurnoutListReply();
410                    break;
411                } else {
412                    log.error("Unknown Output Command: '{}'", msg.toString());
413                    r = "X";
414                }
415                reply = DCCppReply.parseDCCppReply(r);
416                log.debug("Reply generated = '{}'", reply);
417                break;
418
419            case DCCppConstants.SENSOR_CMD:
420                if (msg.isSensorAddMessage()) {
421                    log.debug("SENSOR_CMD Add detected");
422                    //s = msg.toString();
423                    r = "O"; // TODO: Randomize?
424                } else if (msg.isSensorDeleteMessage()) {
425                    log.debug("SENSOR_CMD Delete detected");
426                    //s = msg.toString();
427                    r = "O"; // TODO: Randomize?
428                } else if (msg.isListSensorsMessage()) {
429                    r = "Q 1 4 1"; // TODO: DO this for real.
430                } else {
431                    log.debug("Invalid SENSOR_CMD detected");
432                    r = "X";
433                }
434                reply = DCCppReply.parseDCCppReply(r);
435                log.debug("Reply generated = '{}'", reply);
436                break;
437
438            case DCCppConstants.PROG_WRITE_CV_BYTE:
439                log.debug("PROG_WRITE_CV_BYTE detected");
440                s = msg.toString();
441                r = "";
442                try {
443                    if (s.matches(DCCppConstants.PROG_WRITE_BYTE_REGEX)) {
444                        p = Pattern.compile(DCCppConstants.PROG_WRITE_BYTE_REGEX);
445                        m = p.matcher(s);
446                        if (!m.matches()) {
447                            log.error("Malformed ProgWriteCVByte Command: {}", s);
448                            return (null);
449                        }
450                        // CMD: <W CV Value CALLBACKNUM CALLBACKSUB>
451                        // Response: <r CALLBACKNUM|CALLBACKSUB|CV Value>
452                        r = "r " + m.group(3) + "|" + m.group(4) + "|" + m.group(1) +
453                                " " + m.group(2);
454                        CVs[Integer.parseInt(m.group(1))] = Integer.parseInt(m.group(2));
455                    } else if (s.matches(DCCppConstants.PROG_WRITE_BYTE_V4_REGEX)) {
456                        p = Pattern.compile(DCCppConstants.PROG_WRITE_BYTE_V4_REGEX);
457                        m = p.matcher(s);
458                        if (!m.matches()) {
459                            log.error("Malformed ProgWriteCVByte Command: {}", s);
460                            return (null);
461                        }
462                        // CMD: <W CV Value>
463                        // Response: <r CV Value>
464                        r = "r " + m.group(1) + " " + m.group(2);
465                        CVs[Integer.parseInt(m.group(1))] = Integer.parseInt(m.group(2));
466                    }
467                    reply = DCCppReply.parseDCCppReply(r);
468                    log.debug("Reply generated = {}", reply.toString());
469                } catch (PatternSyntaxException e) {
470                    log.error("Malformed pattern syntax!");
471                    return (null);
472                } catch (IllegalStateException e) {
473                    log.error("Group called before match operation executed string= {}", s);
474                    return (null);
475                } catch (IndexOutOfBoundsException e) {
476                    log.error("Index out of bounds string= {}", s);
477                    return (null);
478                }
479                break;
480
481            case DCCppConstants.PROG_WRITE_CV_BIT:
482                log.debug("PROG_WRITE_CV_BIT detected");
483                s = msg.toString();
484                try {
485                    p = Pattern.compile(DCCppConstants.PROG_WRITE_BIT_REGEX);
486                    m = p.matcher(s);
487                    if (!m.matches()) {
488                        log.error("Malformed ProgWriteCVBit Command: {}", s);
489                        return (null);
490                    }
491                    // CMD: <B CV BIT Value CALLBACKNUM CALLBACKSUB>
492                    // Response: <r CALLBACKNUM|CALLBACKSUB|CV BIT Value>
493                    r = "r " + m.group(4) + "|" + m.group(5) + "|" + m.group(1) + " "
494                            + m.group(2) + m.group(3);
495                    int idx = Integer.parseInt(m.group(1));
496                    int bit = Integer.parseInt(m.group(2));
497                    int v = Integer.parseInt(m.group(3));
498                    if (v == 1) {
499                        CVs[idx] = CVs[idx] | (0x0001 << bit);
500                    } else {
501                        CVs[idx] = CVs[idx] & ~(0x0001 << bit);
502                    }
503                    reply = DCCppReply.parseDCCppReply(r);
504                    log.debug("Reply generated = {}", reply.toString());
505                } catch (PatternSyntaxException e) {
506                    log.error("Malformed pattern syntax!");
507                    return (null);
508                } catch (IllegalStateException e) {
509                    log.error("Group called before match operation executed string= {}", s);
510                    return (null);
511                } catch (IndexOutOfBoundsException e) {
512                    log.error("Index out of bounds string= {}", s);
513                    return (null);
514                }
515                break;
516
517            case DCCppConstants.PROG_READ_CV:
518                log.debug("PROG_READ_CV detected");
519                s = msg.toString();
520                r = "";
521                try {
522                    if (s.matches(DCCppConstants.PROG_READ_CV_REGEX)) {
523                        p = Pattern.compile(DCCppConstants.PROG_READ_CV_REGEX);
524                        m = p.matcher(s);
525                        int cv = Integer.parseInt(m.group(1));
526                        int cvVal = 0; // Default to 0 if they're reading out of bounds.
527                        if (cv < CVs.length) {
528                            cvVal = CVs[Integer.parseInt(m.group(1))];
529                        }
530                        // CMD: <R CV CALLBACKNUM CALLBACKSUB>
531                        // Response: <r CALLBACKNUM|CALLBACKSUB|CV Value>
532                        r = "r " + m.group(2) + "|" + m.group(3) + "|" + m.group(1) + " "
533                                + cvVal;
534                    } else if (s.matches(DCCppConstants.PROG_READ_CV_V4_REGEX)) {
535                        p = Pattern.compile(DCCppConstants.PROG_READ_CV_V4_REGEX);
536                        m = p.matcher(s);
537                        if (!m.matches()) {
538                            log.error("Malformed PROG_READ_CV Command: {}", s);
539                            return (null);
540                        }
541                        int cv = Integer.parseInt(m.group(1));
542                        int cvVal = 0; // Default to 0 if they're reading out of bounds.
543                        if (cv < CVs.length) {
544                            cvVal = CVs[Integer.parseInt(m.group(1))];
545                        }
546                        // CMD: <R CV>
547                        // Response: <r CV Value>
548                        r = "r " + m.group(1) + " " + cvVal;
549                    } else if (s.matches(DCCppConstants.PROG_READ_LOCOID_REGEX)) {
550                        int locoId = ThreadLocalRandom.current().nextInt(9999)+1; //get a random locoId between 1 and 9999
551                        // CMD: <R>
552                        // Response: <r LocoId>
553                        r = "r " + locoId;
554                    } else {
555                        log.error("Malformed PROG_READ_CV Command: {}", s);
556                        return (null);
557                    }
558
559                    reply = DCCppReply.parseDCCppReply(r);
560                    log.debug("Reply generated = {}", reply.toString());
561                } catch (PatternSyntaxException e) {
562                    log.error("Malformed pattern syntax!");
563                    return (null);
564                } catch (IllegalStateException e) {
565                    log.error("Group called before match operation executed string= {}", s);
566                    return (null);
567                } catch (IndexOutOfBoundsException e) {
568                    log.error("Index out of bounds string= {}", s);
569                    return (null);
570                }
571                break;
572
573            case DCCppConstants.PROG_VERIFY_CV:
574                log.debug("PROG_VERIFY_CV detected");
575                s = msg.toString();
576                try {
577                    p = Pattern.compile(DCCppConstants.PROG_VERIFY_REGEX);
578                    m = p.matcher(s);
579                    if (!m.matches()) {
580                        log.error("Malformed PROG_VERIFY_CV Command: {}", s);
581                        return (null);
582                    }
583                    // TODO: Work Magic Here to retrieve stored value.
584                    // Make sure that CV exists
585                    int cv = Integer.parseInt(m.group(1));
586                    int cvVal = 0; // Default to 0 if they're reading out of bounds.
587                    if (cv < CVs.length) {
588                        cvVal = CVs[cv];
589                    }
590                    // CMD: <V CV STARTVAL>
591                    // Response: <v CV Value>
592                    r = "v " + cv + " " + cvVal;
593
594                    reply = DCCppReply.parseDCCppReply(r);
595                    log.debug("Reply generated = {}", reply.toString());
596                } catch (PatternSyntaxException e) {
597                    log.error("Malformed pattern syntax!");
598                    return (null);
599                } catch (IllegalStateException e) {
600                    log.error("Group called before match operation executed string= {}", s);
601                    return (null);
602                } catch (IndexOutOfBoundsException e) {
603                    log.error("Index out of bounds string= {}", s);
604                    return (null);
605                }
606                break;
607
608            case DCCppConstants.TRACK_POWER_ON:
609                log.debug("TRACK_POWER_ON detected");
610                trackPowerState = true;
611                reply = DCCppReply.parseDCCppReply("p1");
612                break;
613
614            case DCCppConstants.TRACK_POWER_OFF:
615                log.debug("TRACK_POWER_OFF detected");
616                trackPowerState = false;
617                reply = DCCppReply.parseDCCppReply("p0");
618                break;
619
620            case DCCppConstants.READ_MAXNUMSLOTS:
621                log.debug("READ_MAXNUMSLOTS detected");
622                reply = DCCppReply.parseDCCppReply("# 12");
623                break;
624
625            case DCCppConstants.READ_TRACK_CURRENT:
626                log.debug("READ_TRACK_CURRENT detected");
627                generateMeterReplies();
628                break;
629
630            case DCCppConstants.TRACKMANAGER_CMD:
631                log.debug("TRACKMANAGER_CMD detected");
632                reply = DCCppReply.parseDCCppReply("= A MAIN");
633                writeReply(reply);
634                reply = DCCppReply.parseDCCppReply("= B PROG");
635                writeReply(reply);
636                reply = DCCppReply.parseDCCppReply("= C MAIN");
637                writeReply(reply);
638                reply = DCCppReply.parseDCCppReply("= D MAIN");
639                break;
640
641            case DCCppConstants.LCD_TEXT_CMD:
642                log.debug("LCD_TEXT_CMD detected");
643                DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss a");
644                LocalDateTime now = LocalDateTime.now();
645                String dateTimeString = now.format(formatter);
646                reply = DCCppReply.parseDCCppReply("@ 0 0 \"Welcome to DCC-EX -- " + dateTimeString + "\"" );
647                writeReply(reply);
648                reply = DCCppReply.parseDCCppReply("@ 0 1 \"LCD Line 1\"");
649                writeReply(reply);
650                reply = DCCppReply.parseDCCppReply("@ 0 2 \"LCD Line 2\"");
651                writeReply(reply);
652                reply = DCCppReply.parseDCCppReply("@ 0 3 \"     LCD Line 3 with spaces   \"");
653                writeReply(reply);
654                reply = DCCppReply.parseDCCppReply("@ 0 4 \"1234567890123456789012345678901234567890\"");
655                break;
656
657            case DCCppConstants.READ_CS_STATUS:
658                log.debug("READ_CS_STATUS detected");
659                generateReadCSStatusReply(); // Handle this special.
660                break;
661
662            case DCCppConstants.THROTTLE_COMMANDS:
663                log.debug("THROTTLE_COMMANDS detected");
664                if (msg.isCurrentMaxesMessage()) {
665                    reply = DCCppReply.parseDCCppReply("jG 4998 4998 4998 4998");
666                } else if (msg.isCurrentValuesMessage()) {
667                    generateCurrentValuesReply(); // Handle this special.
668                } else if (msg.isAutomationIDsMessage()) {
669                    reply = DCCppReply.parseDCCppReply("jA 1 2");
670                } else if (msg.isAutomationIDMessage()) {
671                    reply = generateAutomationIDReply(msg.getAutomationIDInt());
672                }
673                break;
674
675            case DCCppConstants.FUNCTION_CMD:
676            case DCCppConstants.FORGET_CAB_CMD:
677            case DCCppConstants.ACCESSORY_CMD:
678            case DCCppConstants.OPS_WRITE_CV_BYTE:
679            case DCCppConstants.OPS_WRITE_CV_BIT:
680            case DCCppConstants.WRITE_DCC_PACKET_MAIN:
681            case DCCppConstants.WRITE_DCC_PACKET_PROG:
682                log.debug("non-reply message detected: '{}'", msg);
683                // Send no reply.
684                return (null);
685
686            default:
687                log.debug("unknown message detected: '{}'", msg);
688                return (null);
689        }
690        return (reply);
691    }
692
693    //calc speedByte value matching DCC-EX, then store it, so it can be used in the locoState replies
694    private void storeLocoSpeedByte(int locoId, int speed, int dir) {
695        if (speed>0) speed++; //add 1 to speed if not zero or estop
696        if (speed<0) speed = 1; //eStop is actually 1
697        int dirBit = dir*128; //calc value for direction bit
698        int speedByte = dirBit + speed; //add dirBit to adjusted speed value
699        locoSpeedByte.put(locoId, speedByte); //store it
700        if (!locoFunctions.containsKey(locoId)) locoFunctions.put(locoId, 0); //init functions if not set
701    }
702
703    //stores the calculated value of the functionsByte as used by DCC-EX
704    private void storeLocoFunction(int locoId, int function, int state) {
705        int functions = 0; //init functions to all off if not stored
706        if (locoFunctions.containsKey(locoId))
707            functions = locoFunctions.get(locoId); //get stored value, if any
708        int mask = 1 << function;
709        if (state == 1) {
710            functions = functions | mask; //apply ON
711        } else {
712            functions = functions & ~mask; //apply OFF
713        }
714        locoFunctions.put(locoId, functions); //store new value
715        if (!locoSpeedByte.containsKey(locoId))
716            locoSpeedByte.put(locoId, 0); //init speedByte if not set
717    }
718
719    //retrieve stored values and calculate and format the locostate message text
720    private String getLocoStateString(int locoId) {
721        String s;
722        int speedByte = locoSpeedByte.get(locoId);
723        int functions = locoFunctions.get(locoId);
724        s = "l " + locoId + " 0 " + speedByte + " " + functions;  //<l loco slot speedByte functions>
725        return s;
726    }
727
728    private DCCppReply generateAutomationIDReply(int id) {
729        switch (id) {
730            case 1: return DCCppReply.parseDCCppReply("jA 1 R \"Simulator Route\"");
731            case 2: return DCCppReply.parseDCCppReply("jA 2 A \"Simulator Auto\"");
732            default: return null;
733        }
734    }
735
736    /* 's'tatus message gets multiple reply messages */
737    private void generateReadCSStatusReply() {
738        DCCppReply r = new DCCppReply("p" + (trackPowerState ? "1" : "0"));
739        writeReply(r);
740        r = DCCppReply.parseDCCppReply("iDCC-EX V-5.0.4 / MEGA / STANDARD_MOTOR_SHIELD G-9db6d36");
741        writeReply(r);
742        generateTurnoutStatesReply();
743    }
744
745    /* Send list of creation command with states for all defined turnouts and outputs */
746    private void generateTurnoutListReply() {
747        if (!turnouts.isEmpty()) {
748            turnouts.forEach((key, value) -> { //send back the full create string for each
749                DCCppReply r = new DCCppReply(value);
750                writeReply(r);
751            });
752        } else {
753            writeReply(new DCCppReply("X No Turnouts Defined"));
754        }
755    }
756
757    /* Send list of turnout states */
758    private void generateTurnoutStatesReply() {
759        if (!turnouts.isEmpty()) {
760            turnouts.forEach((key, value) -> {
761                String s = value.substring(0,2) + key + value.substring(value.length()-2); //command char + id + state
762                DCCppReply r = new DCCppReply(s);
763                writeReply(r);
764            });
765        } else {
766            writeReply(new DCCppReply("X No Turnouts Defined"));
767        }
768    }
769
770    /* 'c' current request message gets multiple reply messages */
771    private void generateMeterReplies() {
772        int currentmA = 1100 + ThreadLocalRandom.current().nextInt(64);
773        double voltageV = 14.5 + ThreadLocalRandom.current().nextInt(10)/10.0;
774        String rs = "c CurrentMAIN " + (trackPowerState ? Double.toString(currentmA) : "0") + " C Milli 0 1997 1 1997";
775        DCCppReply r = new DCCppReply(rs);
776        writeReply(r);
777        r = new DCCppReply("c VoltageMAIN " + voltageV + " V NoPrefix 0 18.0 0.1 16.0");
778        writeReply(r);
779    }
780
781    /* 'JI' Current Value List request message returns an array of Current Values */
782    private void generateCurrentValuesReply() {
783        int currentmA_0 = 1100 + ThreadLocalRandom.current().nextInt(64);
784        int currentmA_1 = 0500 + ThreadLocalRandom.current().nextInt(64);
785        int currentmA_2 = 1100 + ThreadLocalRandom.current().nextInt(64);
786        int currentmA_3 = 1100 + ThreadLocalRandom.current().nextInt(64);
787        String rs = "jI " + (trackPowerState ? Integer.toString(currentmA_0) : "0") + " " +
788                (trackPowerState ? Integer.toString(currentmA_1) : "0") + " " +
789                (trackPowerState ? Integer.toString(currentmA_2) : "0") + " " +
790                (trackPowerState ? Integer.toString(currentmA_3) : "0");
791        DCCppReply r = new DCCppReply(rs);
792        writeReply(r);
793    }
794
795    private void generateRandomSensorReply() {
796        // Pick a random sensor number between 0 and 10;
797        int sensorNum = ThreadLocalRandom.current().nextInt(10)+1; // Generate a random sensor number between 1 and 10
798        int value = ThreadLocalRandom.current().nextInt(2); // Generate state value between 0 and 1
799
800        String reply = (value == 1 ? "Q " : "q ") + sensorNum;
801
802        DCCppReply r = DCCppReply.parseDCCppReply(reply);
803        writeReply(r);
804    }
805
806    private void writeReply(DCCppReply r) {
807        log.debug("Simulator Thread sending Reply '{}'", r);
808        int i;
809        int len = r.getLength();  // opCode+Nbytes+ECC
810        // If r == null, there is no reply to be sent.
811        try {
812            outpipe.writeByte((byte) '<');
813            for (i = 0; i < len; i++) {
814                outpipe.writeByte((byte) r.getElement(i));
815            }
816            outpipe.writeByte((byte) '>');
817        } catch (java.io.IOException ex) {
818            ConnectionStatus.instance().setConnectionState(
819                    getSystemConnectionMemo(), ConnectionStatus.CONNECTION_DOWN);
820        }
821    }
822
823    /**
824     * Get characters from the input source, and file a message.
825     * <p>
826     * Returns only when the message is complete.
827     * <p>
828     * Only used in the Receive thread.
829     *
830     * @return filled message
831     * @throws IOException when presented by the input source.
832     */
833    private DCCppMessage loadChars() throws java.io.IOException {
834        // Spin waiting for start-of-frame '<' character (and toss it)
835        StringBuilder s = new StringBuilder();
836        byte char1;
837        boolean found_start = false;
838
839        // this loop reads every other character; is that the desired behavior?
840        while (!found_start) {
841            char1 = readByteProtected(inpipe);
842            if ((char1 & 0xFF) == '<') {
843                found_start = true;
844                log.trace("Found starting < ");
845                break; // A bit redundant with setting the loop condition true (false)
846            } else {
847                // drop next character before repeating
848                readByteProtected(inpipe);
849            }
850        }
851        // Now, suck in the rest of the message...
852        for (int i = 0; i < DCCppConstants.MAX_MESSAGE_SIZE; i++) {
853            char1 = readByteProtected(inpipe);
854            if (char1 == '>') {
855                log.trace("msg found > ");
856                // Don't store the >
857                break;
858            } else {
859                log.trace("msg read byte {}", char1);
860                char c = (char) (char1 & 0x00FF);
861                s.append(c);
862            }
863        }
864        // TODO: Still need to strip leading and trailing whitespace.
865        log.debug("Complete message = {}", s);
866        return (new DCCppMessage(s.toString()));
867    }
868
869    /**
870     * Read a single byte, protecting against various timeouts, etc.
871     * <p>
872     * When a port is set to have a receive timeout (via the
873     * enableReceiveTimeout() method), some will return zero bytes or an
874     * EOFException at the end of the timeout. In that case, the read should be
875     * repeated to get the next real character.
876     * @param istream source of data
877     * @return next available byte, when available
878     * @throws IOException from underlying operation
879     *
880     */
881    protected byte readByteProtected(DataInputStream istream) throws java.io.IOException {
882        byte[] rcvBuffer = new byte[1];
883        while (true) { // loop will repeat until character found
884            int nchars;
885            nchars = istream.read(rcvBuffer, 0, 1);
886            if (nchars > 0) {
887                return rcvBuffer[0];
888            }
889        }
890    }
891
892    volatile static DCCppSimulatorAdapter mInstance = null;
893    private DataOutputStream pout = null; // for output to other classes
894    private DataInputStream pin = null; // for input from other classes
895    // internal ends of the pipes
896    private DataOutputStream outpipe = null;  // feed pin
897    private DataInputStream inpipe = null; // feed pout
898    private Thread sourceThread;
899
900    private static final Logger log = LoggerFactory.getLogger(DCCppSimulatorAdapter.class);
901
902}