001package jmri.jmrix.nce.simulator;
002
003import java.io.*;
004import java.util.Arrays;
005
006import org.slf4j.Logger;
007import org.slf4j.LoggerFactory;
008
009import jmri.jmrix.nce.*;
010import jmri.jmrix.nce.NceCmdStationMemory;
011import jmri.util.ImmediatePipedOutputStream;
012
013/**
014 * The following was copied from the NCE Power Pro System Reference Manual. It
015 * provides the background for the various NCE command that are simulated by
016 * this implementation.
017 * <p>
018 * Implements a Command Station Simulator for the NCE system.
019 * <p>
020 * BINARY COMMAND SET
021 * <p>
022 * The RS-232 port binary commands are designed to work in a computer friendly
023 * way. Command format is: {@code <command number> <data> <data> ...}
024 * <p>
025 * Commands range from 0x80 to 0xBF
026 * <p>
027 * Commands and formats supported: Commands 0xAD to 0xBF are not used and return
028 * '0'
029 * <p>
030 * Errors returned: '0'= command not supported '1'= loco address out of range
031 * '2'= cab address out of range '3'= data out of range '4'= byte count out of
032 * range '!'= command completed successfully
033 * <p>
034 * For a complete description of Binary Commands see:
035 * www.ncecorporation.com/pdf/bincmds.pdf
036 * <br>
037 * <pre>{@literal
038 * Command Description (#bytes rtn) Responses
039 * 0x80 NOP, dummy instruction (1) !
040 * 0x81 xx xx yy assign loco xxxx to cab cc (1) !, 1,2
041 * 0x82 read clock (2) <hours><minutes>
042 * 0x83 Clock stop (1) !
043 * 0x84 Clock start (1) !
044 * 0x85 xx xx Set clock hr./min (1) !,3
045 * 0x86 xx Set clock 12/24 (1) !,3
046 * 0x87 xx Set clock ratio (1) !,3
047 * 0x88 xxxx Dequeue packet by loco addr (1) !, 1,2
048 * 0x89 Enable main trk, kill prog (1) !
049 * 0x8A yy Return status of AIU yy (4) <current hi byte> <current lo byte> <change hi byte> <change lo byte>
050 * 0x8B Kill main trk, enable prog (1) !
051 * 0x8C dummy inst. returns "!" followed CR/LF(3) !, 0x0D, 0x0A
052 * 0x8D xxxx mm Set speed mode of loco xxxx to mode mm, 1=14, 2=28, 3=128 (1) !, 1,3<speed mode, 0 to 3>
053 * 0x8E aaaa nn<16 data bytes> Write nn bytes, start at aaaa Must have 16 data bytes, pad them out to 16 if necessary (1) !,4
054 * 0x8F aaaa Read 16 bytes, start at aaaa(16) 16 bytes
055 * 0x90 cc xx... Send 16 char message to Cab ccLCD line 3. xx = 16 ASCII char (1) ! ,2
056 * 0x91 cc xx Send 16 char message to cab cc LCD line 4. xx=16 ASCII (1) !,2
057 * 0x92 cc xx Send 8 char message to cab cc LCD line 2 right xx=8 char (1) !,2
058 * 0x93 ss<3 byte packet> Queue 3 byte packet to temp _Q send ss times (1) !
059 * 0x94 ss<4 byte packet> Queue 4 byte packet to temp _Q send ss times (1) !
060 * 0x95 ss<5 byte packet> Queue 5 byte packet to temp_Q send ss times (1) !
061 * 0x96 ss<6 byte packet> Queue 6 byte packet to temp _Q send ss times (1) !
062 * 0x97 aaaa xx Write 1 byte to aaaa (1) !
063 * 0x98 aaaa xx xxWrite 2 bytes to aaaa (1) !
064 * 0x99 aaaa<4 data bytes> Write 4 bytes to aaaa (1) !
065 * 0x9A aaaa<8 data bytes> Write 8 bytes to aaaa (1) !
066 * 0x9B yy Return status of AIU yy (short form of command 0x8A) (2) <current hi byte><current lo byte><br>
067 * 0x9C xx Execute macro number xx (1) !, 0,3
068 * 0x9D aaaa Read 1 byte from aaaa (1) 1 byte
069 * 0x9E Enter programming track mode(1) !=success 3=short circuit
070 * 0x9F Exit programming track mode (1) !=success
071 * 0xA0 aaaa xx Program CV aa with data xx in paged mode (1) !=success 0=program track
072 * 0xA1 aaaa Read CV aaaa in paged mode Note: cv data followed by ! for OK. 0xFF followed by 3 for can't read CV (2) !, 0,3
073 * 0xA2<4 data bytes> Locomotive control command (1) !,1
074 * 0xA3<3 bytepacket> Queue 3 byte packet to TRK _Q (replaces any packet with same address if it exists) (1) !,1
075 * 0xA4<4 byte packet> Queue 4 byte packet to TRK _Q (1) !,1
076 * 0xA5<5 byte packet> Queue 5 byte packet to TRK _Q (1) !,1
077 * 0xA6 rr dd Program register rr with dd (1) !=success 0=no program track
078 * 0xA7 rr Read register rr. Note: cv data followed by ! for OK. 0xFF followed by 3 for can't read CV (2) !,3 0=no program track
079 * 0xA8 aaaa dd Program CV aaaa with dd in direct mode. (1) !=success 0=no program track
080 * 0xA9 aaaa Read CV aaaa in direct mode. Note: cv data followed by ! for OK.
081 * 0xFF followed by 3 for can't read CV (2) !,3
082 * 0xAA Return software revision number. Format: VV.MM.mm (3) 3 data bytes
083 * 0xAB Perform soft reset of command station (like cycling power) (0) Returns nothing
084 * 0xAC Perform hard reset of command station. Reset to factory defaults (Note: will change baud rate to 9600)(0) Returns nothing
085 * 0xAD <4 data bytes>Accy/signal and macro commands (1) !,1
086 * }</pre>
087 *
088 * @author Bob Jacobsen Copyright (C) 2001, 2002
089 * @author Paul Bender, Copyright (C) 2009
090 * @author Daniel Boudreau Copyright (C) 2010
091 * @author Ken Cameron Copyright (C) 2023
092 */
093public class SimulatorAdapter extends NcePortController implements Runnable {
094
095    NceTrafficController tc;
096    // private control members
097    private Thread sourceThread;
098
099    // streams to share with user class
100    private DataOutputStream pout = null; // this is provided to classes who want to write to us
101    private DataInputStream pin = null; // this is provided to classes who want data from us
102
103    // internal ends of the pipes
104    private DataOutputStream outpipe = null; // feed pin
105    private DataInputStream inpipe = null; // feed pout
106
107    // Simulator responses
108    char NCE_OKAY = '!';
109    char NCE_ERROR = '0';
110    char NCE_LOCO_OUT_OF_RANGE = '1';
111    char NCE_CAB_OUT_OF_RANGE = '2';
112    char NCE_DATA_OUT_OF_RANGE = '3';
113    char NCE_BYTE_OUT_OF_RANGE = '4';
114
115    /**
116     * Create a new SimulatorAdapter.
117     */
118    public SimulatorAdapter() {
119        super(new NceSystemConnectionMemo());
120        option1Name = "Eprom"; // NOI18N
121        options.put(option1Name, new Option(Bundle.getMessage("EpromLabel"), option1Values, false));
122        setOptionState(option1Name, option1Values[1]);
123    }
124
125    /**
126     * {@inheritDoc} Simulated input/output pipes.
127     */
128    @Override
129    public String openPort(String portName, String appName) {
130        try {
131            PipedOutputStream tempPipeI = new ImmediatePipedOutputStream();
132            pout = new DataOutputStream(tempPipeI);
133            inpipe = new DataInputStream(new PipedInputStream(tempPipeI));
134            PipedOutputStream tempPipeO = new ImmediatePipedOutputStream();
135            outpipe = new DataOutputStream(tempPipeO);
136            pin = new DataInputStream(new PipedInputStream(tempPipeO));
137        } catch (java.io.IOException e) {
138            log.error("init (pipe): Exception: ", e);
139        }
140        opened = true;
141        return null; // indicates OK return
142    }
143
144    // Warning: EPROM revision must match option1Values array index value.
145    String[] option1Values = new String[]{"2006 to Mar 1 2007", "Mar 3 2007 to Jan 24 2008", "Feb 2 2008 to Jan 30 2021", "Feb 22 2021 on"}; // NOI18N
146    int epromRevision = -1;
147
148    /**
149     * Set up all of the other objects to simulate operation with an NCE command
150     * station.
151     */
152    @Override
153    public void configure() {
154        tc = new NceTrafficController();
155        this.getSystemConnectionMemo().setNceTrafficController(tc);
156        tc.setAdapterMemo(this.getSystemConnectionMemo());
157        tc.connectPort(this);
158        tc.setSimulatorRunning(true);
159
160        // setting binary mode
161        this.getSystemConnectionMemo().configureCommandStation(NceTrafficController.OPTION_2006);
162        tc.setCmdGroups(NceTrafficController.CMDS_MEM
163                | NceTrafficController.CMDS_AUI_READ
164                | NceTrafficController.CMDS_PROGTRACK
165                | NceTrafficController.CMDS_OPS_PGM
166                | NceTrafficController.CMDS_USB
167                | NceTrafficController.CMDS_NOT_USB
168                | NceTrafficController.CMDS_CLOCK
169                | NceTrafficController.CMDS_ALL_SYS);
170        tc.setUsbSystem(NceTrafficController.USB_SYSTEM_NONE);
171
172        epromRevision = Arrays.asList(option1Values).indexOf(getOptionState(option1Name));
173        if (epromRevision == -1) {
174            epromRevision = 1;  // default revision if no match
175        }
176
177        this.getSystemConnectionMemo().configureManagers();
178        tc.csm = new NceCmdStationMemory();
179
180        // start the simulator
181        sourceThread = new Thread(this);
182        sourceThread.setName("Nce Simulator");
183        sourceThread.setPriority(Thread.MIN_PRIORITY);
184        sourceThread.start();
185    }
186
187    // Base class methods for the NcePortController interface.
188    /**
189     * {@inheritDoc}
190     */
191    @Override
192    public DataInputStream getInputStream() {
193        if (!opened || pin == null) {
194            log.error("getInputStream called before load(), stream not available");
195        }
196        return pin;
197    }
198
199    /**
200     * {@inheritDoc}
201     */
202    @Override
203    public DataOutputStream getOutputStream() {
204        if (!opened || pout == null) {
205            log.error("getOutputStream called before load(), stream not available");
206        }
207        return pout;
208    }
209
210    /**
211     * {@inheritDoc}
212     */
213    @Override
214    public boolean status() {
215        return opened;
216    }
217
218    /**
219     * {@inheritDoc}
220     *
221     * @return null
222     */
223    @Override
224    public String[] validBaudRates() {
225        log.debug("validBaudRates should not have been invoked");
226        return new String[]{};
227    }
228
229    /**
230     * {@inheritDoc}
231     *
232     * @return null
233     */
234    @Override
235    public int[] validBaudNumbers() {
236        return new int[]{};
237    }
238
239    @Override
240    public String getCurrentBaudRate() {
241        return "";
242    }
243
244    @Override
245    public String getCurrentPortName() {
246        return "";
247    }
248
249    @Override
250    public void run() { // start a new thread
251        // This thread has one task.  It repeatedly reads from the input pipe
252        // and writes an appropriate response to the output pipe. This is the heart
253        // of the NCE command station simulation.
254        // report status?
255        log.info("NCE Simulator Started");
256        while (true) {
257            NceMessage m = readMessage();
258            if (log.isDebugEnabled()) {
259                StringBuilder buf = new StringBuilder();
260                for (int i = 0; i < m.getNumDataElements(); i++) {
261                    buf.append(Integer.toHexString(0xFF & m.getElement(i)).toUpperCase()).append(" ");
262                }
263                log.debug("Nce simulator received message: {}", buf );
264            }
265            if (m != null) {
266                NceReply r = generateReply(m);
267                writeReply(r);
268                if (log.isDebugEnabled() && r != null) {
269                    StringBuilder buf = new StringBuilder();
270                    for (int i = 0; i < r.getNumDataElements(); i++) {
271                        buf.append(Integer.toHexString(0xFF & r.getElement(i)).toUpperCase()).append(" ");
272                    }
273                    log.debug("Nce simulator sent reply: {}", buf.toString());
274                }
275            }
276        }
277    }
278
279    // readMessage reads one incoming message from the buffer
280    private NceMessage readMessage() {
281        NceMessage msg = null;
282        try {
283            msg = loadChars();
284        } catch (java.io.IOException e) {
285
286        }
287        return (msg);
288    }
289
290    /**
291     * Get characters from the input source.
292     *
293     * @return filled message
294     * @throws IOException when presented by the input source.
295     */
296    private NceMessage loadChars() throws java.io.IOException {
297        int nchars;
298        byte[] rcvBuffer = new byte[32];
299
300        nchars = inpipe.read(rcvBuffer, 0, 32);
301        //log.debug("new message received");
302        NceMessage msg = new NceMessage(nchars);
303
304        for (int i = 0; i < nchars; i++) {
305            msg.setElement(i, rcvBuffer[i] & 0xFF);
306        }
307        return msg;
308    }
309
310    /**
311     * This is the heart of the simulation. It translates an incoming NceMessage
312     * into an outgoing NceReply.
313     */
314    private NceReply generateReply(NceMessage m) {
315        NceReply reply = new NceReply(this.getSystemConnectionMemo().getNceTrafficController());
316        int command = m.getElement(0);
317        if (command < 0x80) {  // NOTE: NCE command station does not respond to
318            return null;      // command less than 0x80 (times out)
319        }
320        if (command > 0xBF) { // Command is out of range
321            reply.setElement(0, NCE_ERROR);  // Nce command not supported
322            return reply;
323        }
324        switch (command) {
325            case NceMessage.SW_REV_CMD:  // Get EPROM revision
326                reply.setElement(0, 0x06);    // Send EPROM revision based on Preference setting
327                reply.setElement(1, 0x02);
328                reply.setElement(2, epromRevision);
329                break;
330            case NceMessage.READ_CLOCK_CMD: // Read clock
331                reply.setElement(0, 0x12);   // Return fixed time
332                reply.setElement(1, 0x30);
333                break;
334            case NceMessage.READ_AUI4_CMD: // Read AUI 4 byte response
335                reply.setElement(0, 0xFF);   // fixed data for now
336                reply.setElement(1, 0xFF);   // fixed data for now
337                reply.setElement(2, 0x00);   // fixed data for now
338                reply.setElement(3, 0x00);   // fixed data for now
339                break;
340            case NceMessage.DUMMY_CMD:  // Dummy instruction
341                reply.setElement(0, NCE_OKAY);  // return ! CR LF
342                reply.setElement(1, 0x0D);
343                reply.setElement(2, 0x0A);
344                break;
345            case NceMessage.READ16_CMD:  // Read 16 bytes
346                readMemory(m, reply, 16);
347                break;
348            case NceMessage.READ_AUI2_CMD: // Read AUI 2 byte response
349                reply.setElement(0, 0x00);   // fixed data for now
350                reply.setElement(1, 0x00);   // fixed data for now
351                break;
352            case NceMessage.READ1_CMD:  // Read 1 bytes
353                readMemory(m, reply, 1);
354                break;
355            case NceMessage.WRITE1_CMD:  // Write 1 bytes
356                writeMemory(m, reply, 1, false);
357                break;
358            case NceMessage.WRITE2_CMD:  // Write 2 bytes
359                writeMemory(m, reply, 2, false);
360                break;
361            case NceMessage.WRITE4_CMD:  // Write 4 bytes
362                writeMemory(m, reply, 4, false);
363                break;
364            case NceMessage.WRITE8_CMD:  // Write 8 bytes
365                writeMemory(m, reply, 8, false);
366                break;
367            case NceMessage.WRITE_N_CMD:  // Write n bytes
368                writeMemory(m, reply, m.getElement(3), true);
369                break;
370            case NceMessage.SEND_ACC_SIG_MACRO_CMD:   // accessory command
371                accessoryCommand(m, reply);
372                break;
373            case NceMessage.READ_DIR_CV_CMD:
374            case NceMessage.READ_PAGED_CV_CMD:
375            case NceMessage.READ_REG_CMD:
376                reply.setElement(0, 123);   // dummy data
377                reply.setElement(1, NCE_OKAY);  // forces succeed
378                // Sample code to modify simulator response for testing purposes.
379                // Uncomment and modify as desired.
380//                int cvnum = (m.getElement(1) << 8) | (m.getElement(2));
381//                if (cvnum == 7) {
382//                    reply.setElement(0, 88);  // forces fail
383//                }
384//                if (cvnum == 8) {
385//                    reply.setElement(0, 48);  // forces Hornby
386//                }
387//                if (cvnum == 159) {
388//                    reply.setElement(0, 145);
389//                    reply.setElement(1, NCE_DATA_OUT_OF_RANGE);  // forces fail
390//                }
391                break;
392            default:
393                reply.setElement(0, NCE_OKAY);   // Nce okay reply!
394        }
395        return reply;
396    }
397
398    /**
399     * Write reply to output.
400     *
401     * @param r reply on message
402     */
403    private void writeReply(NceReply r) {
404        if (r == null) {
405            return;
406        }
407        for (int i = 0; i < r.getNumDataElements(); i++) {
408            try {
409                outpipe.writeByte((byte) r.getElement(i));
410            } catch (java.io.IOException ex) {
411            }
412        }
413        try {
414            outpipe.flush();
415        } catch (java.io.IOException ex) {
416        }
417    }
418
419    private final byte[] turnoutMemory = new byte[256];
420    private final byte[] macroMemory = new byte[256 * 20 + 16]; // and a little padding
421    private final byte[] consistMemory = new byte[256 * 6 + 16]; // and a little padding
422
423    /* Read NCE memory.  This implementation simulates reading the NCE
424     * command station memory.  There are three memory blocks that are
425     * supported, turnout status, macros, and consists.  The turnout status
426     * memory is 256 bytes and starts at memory address 0xEC00. The macro memory
427     * is 256*20 or 5120 bytes and starts at memory address 0xC800 (PH5 0x6000). The consist
428     * memory is 256*6 or 1536 bytes and starts at memory address 0xF500 (PH5 0x4E00).
429     *
430     */
431    private NceReply readMemory(NceMessage m, NceReply reply, int num) {
432        if (num > 16) {
433            log.error("Nce read memory command was greater than 16");
434            return null;
435        }
436        int nceMemoryAddress = getNceAddress(m);
437        if (nceMemoryAddress >= tc.csm.getAccyMemAddr() && nceMemoryAddress < tc.csm.getAccyMemAddr() + tc.csm.getAccyMemSize()) {
438            log.debug("Reading turnout memory: {}", Integer.toHexString(nceMemoryAddress));
439            int offset = m.getElement(2);
440            for (int i = 0; i < num; i++) {
441                reply.setElement(i, turnoutMemory[offset + i]);
442            }
443            return reply;
444        }
445        if (nceMemoryAddress >= tc.csm.getConsistHeadAddr() && nceMemoryAddress < tc.csm.getConsistHeadAddr() + tc.csm.getConsistMidSize()) {
446            log.debug("Reading consist memory: {}", Integer.toHexString(nceMemoryAddress));
447            int offset = nceMemoryAddress - tc.csm.getConsistHeadAddr();
448
449            for (int i = 0; i < num; i++) {
450                reply.setElement(i, consistMemory[offset + i]);
451            }
452            return reply;
453        }
454        if (nceMemoryAddress >= tc.csm.getMacroAddr() && nceMemoryAddress < tc.csm.getMacroAddr() + tc.csm.getMacroSize()) {
455            log.debug("Reading macro memory: {}", Integer.toHexString(nceMemoryAddress));
456            int offset = nceMemoryAddress - tc.csm.getMacroAddr();
457            log.debug("offset: {}", offset);
458            for (int i = 0; i < num; i++) {
459                reply.setElement(i, macroMemory[offset + i]);
460            }
461            return reply;
462        }
463        for (int i = 0; i < num; i++) {
464            reply.setElement(i, 0x00);   // default fixed data
465        }
466        return reply;
467    }
468
469    private NceReply writeMemory(NceMessage m, NceReply reply, int num, boolean skipbyte) {
470        if (num > 16) {
471            log.error("Nce write memory command was greater than 16");
472            return null;
473        }
474        int nceMemoryAddress = getNceAddress(m);
475        int byteDataBegins = 3;
476        if (skipbyte) {
477            byteDataBegins++;
478        }
479        if (nceMemoryAddress >= tc.csm.getMacroAddr() && nceMemoryAddress < tc.csm.getMacroAddr() + 256) {
480            log.debug("Writing turnout memory: {}", Integer.toHexString(nceMemoryAddress));
481            int offset = m.getElement(2);
482            for (int i = 0; i < num; i++) {
483                turnoutMemory[offset + i] = (byte) m.getElement(i + byteDataBegins);
484            }
485        }
486        if (nceMemoryAddress >= tc.csm.getMacroAddr() && nceMemoryAddress < tc.csm.getMacroAddr() + 256 * 6) {
487            log.debug("Writing consist memory: {}", Integer.toHexString(nceMemoryAddress));
488            int offset = nceMemoryAddress - tc.csm.getMacroAddr();
489            for (int i = 0; i < num; i++) {
490                consistMemory[offset + i] = (byte) m.getElement(i + byteDataBegins);
491            }
492        }
493        if (nceMemoryAddress >= tc.csm.getMacroAddr() && nceMemoryAddress < tc.csm.getMacroAddr() + 256 * 20) {
494            log.debug("Writing macro memory: {}", Integer.toHexString(nceMemoryAddress));
495            int offset = nceMemoryAddress - tc.csm.getMacroAddr();
496            log.debug("offset: {}", offset);
497            for (int i = 0; i < num; i++) {
498                macroMemory[offset + i] = (byte) m.getElement(i + byteDataBegins);
499            }
500        }
501        reply.setElement(0, NCE_OKAY);   // Nce okay reply!
502        return reply;
503    }
504
505    /**
506     * Extract item address from a message.
507     *
508     * @param m received message
509     * @return address from the message
510     */
511    private int getNceAddress(NceMessage m) {
512        int addr = m.getElement(1);
513        addr = addr * 256;
514        addr = addr + m.getElement(2);
515        return addr;
516    }
517
518    private NceReply accessoryCommand(NceMessage m, NceReply reply) {
519        if (m.getElement(3) == 0x03 || m.getElement(3) == 0x04) {  // 0x03 = close, 0x04 = throw
520            String operation = "close";
521            if (m.getElement(3) == 0x04) {
522                operation = "throw";
523            }
524            int nceAccessoryAddress = getNceAddress(m);
525            log.debug("Accessory command {} NT {}", operation, nceAccessoryAddress);
526            if (nceAccessoryAddress > 2044) {
527                log.error("Turnout address greater than 2044, address: {}", nceAccessoryAddress);
528                return null;
529            }
530            int bit = (nceAccessoryAddress - 1) & 0x07;
531            int setMask = 0x01;
532            for (int i = 0; i < bit; i++) {
533                setMask = setMask << 1;
534            }
535            int clearMask = 0x0FFF - setMask;
536            // log.debug("setMask: {} clearMask: {}", Integer.toHexString(setMask), Integer.toHexString(clearMask));
537            int offset = (nceAccessoryAddress - 1) >> 3;
538            int read = turnoutMemory[offset];
539            byte write = (byte) (read & clearMask & 0xFF);
540
541            if (operation.equals("close")) {
542                write = (byte) (write + setMask); // set bit if closed
543            }
544            turnoutMemory[offset] = write;
545            // log.debug("wrote: {}", Integer.toHexString(write));
546        }
547        reply.setElement(0, NCE_OKAY);   // Nce okay reply!
548        return reply;
549    }
550
551    private final static Logger log = LoggerFactory.getLogger(SimulatorAdapter.class);
552
553}