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