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