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