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