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: ", manufacturerName, 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
158        // setting binary mode
159        this.getSystemConnectionMemo().configureCommandStation(NceTrafficController.OPTION_2006);
160        tc.setCmdGroups(NceTrafficController.CMDS_MEM
161                | NceTrafficController.CMDS_AUI_READ
162                | NceTrafficController.CMDS_PROGTRACK
163                | NceTrafficController.CMDS_OPS_PGM
164                | NceTrafficController.CMDS_USB
165                | NceTrafficController.CMDS_NOT_USB
166                | NceTrafficController.CMDS_CLOCK
167                | NceTrafficController.CMDS_ALL_SYS);
168        tc.setUsbSystem(NceTrafficController.USB_SYSTEM_NONE);
169
170        epromRevision = Arrays.asList(option1Values).indexOf(getOptionState(option1Name));
171        if (epromRevision == -1) {
172            epromRevision = 1;  // default revision if no match
173        }
174
175        tc.csm = new NceCmdStationMemory();
176        tc.connectPort(this);
177        tc.setSimulatorRunning(true);
178
179        this.getSystemConnectionMemo().configureManagers();
180
181        // start the simulator
182        sourceThread = new Thread(this);
183        sourceThread.setName("Nce Simulator");
184        sourceThread.setPriority(Thread.MIN_PRIORITY);
185        sourceThread.start();
186    }
187
188    // Base class methods for the NcePortController interface.
189    /**
190     * {@inheritDoc}
191     */
192    @Override
193    public DataInputStream getInputStream() {
194        if (!opened || pin == null) {
195            log.error("getInputStream called before load(), stream not available");
196        }
197        return pin;
198    }
199
200    /**
201     * {@inheritDoc}
202     */
203    @Override
204    public DataOutputStream getOutputStream() {
205        if (!opened || pout == null) {
206            log.error("getOutputStream called before load(), stream not available");
207        }
208        return pout;
209    }
210
211    /**
212     * {@inheritDoc}
213     */
214    @Override
215    public boolean status() {
216        return opened;
217    }
218
219    /**
220     * {@inheritDoc}
221     *
222     * @return null
223     */
224    @Override
225    public String[] validBaudRates() {
226        log.debug("validBaudRates should not have been invoked");
227        return new String[]{};
228    }
229
230    /**
231     * {@inheritDoc}
232     *
233     * @return null
234     */
235    @Override
236    public int[] validBaudNumbers() {
237        return new int[]{};
238    }
239
240    @Override
241    public String getCurrentBaudRate() {
242        return "";
243    }
244
245    @Override
246    public String getCurrentPortName() {
247        return "";
248    }
249
250    @Override
251    public void run() { // start a new thread
252        // This thread has one task.  It repeatedly reads from the input pipe
253        // and writes an appropriate response to the output pipe. This is the heart
254        // of the NCE command station simulation.
255        // report status?
256        log.info("NCE Simulator Started");
257        while (true) {
258            NceMessage m = readMessage();
259            if (log.isDebugEnabled()) {
260                StringBuilder buf = new StringBuilder();
261                for (int i = 0; i < m.getNumDataElements(); i++) {
262                    buf.append(Integer.toHexString(0xFF & m.getElement(i)).toUpperCase()).append(" ");
263                }
264                log.debug("Nce simulator received message: {}", buf );
265            }
266            if (m != null) {
267                NceReply r = generateReply(m);
268                writeReply(r);
269                if (log.isDebugEnabled() && r != null) {
270                    StringBuilder buf = new StringBuilder();
271                    for (int i = 0; i < r.getNumDataElements(); i++) {
272                        buf.append(Integer.toHexString(0xFF & r.getElement(i)).toUpperCase()).append(" ");
273                    }
274                    log.debug("Nce simulator sent reply: {}", buf.toString());
275                }
276            }
277        }
278    }
279
280    // readMessage reads one incoming message from the buffer
281    private NceMessage readMessage() {
282        NceMessage msg = null;
283        try {
284            msg = loadChars();
285        } catch (java.io.IOException e) {
286
287        }
288        return (msg);
289    }
290
291    /**
292     * Get characters from the input source.
293     *
294     * @return filled message
295     * @throws IOException when presented by the input source.
296     */
297    private NceMessage loadChars() throws java.io.IOException {
298        int nchars;
299        byte[] rcvBuffer = new byte[32];
300
301        nchars = inpipe.read(rcvBuffer, 0, 32);
302        //log.debug("new message received");
303        NceMessage msg = new NceMessage(nchars);
304
305        for (int i = 0; i < nchars; i++) {
306            msg.setElement(i, rcvBuffer[i] & 0xFF);
307        }
308        return msg;
309    }
310
311    /**
312     * This is the heart of the simulation. It translates an incoming NceMessage
313     * into an outgoing NceReply.
314     */
315    private NceReply generateReply(NceMessage m) {
316        NceReply reply = new NceReply(this.getSystemConnectionMemo().getNceTrafficController());
317        int command = m.getElement(0);
318        if (command < 0x80) {  // NOTE: NCE command station does not respond to
319            return null;      // command less than 0x80 (times out)
320        }
321        if (command > 0xBF) { // Command is out of range
322            reply.setElement(0, NCE_ERROR);  // Nce command not supported
323            return reply;
324        }
325        switch (command) {
326            case NceMessage.SW_REV_CMD:  // Get EPROM revision
327                reply.setElement(0, 0x06);    // Send EPROM revision based on Preference setting
328                reply.setElement(1, 0x02);
329                reply.setElement(2, epromRevision);
330                break;
331            case NceMessage.READ_CLOCK_CMD: // Read clock
332                reply.setElement(0, 0x12);   // Return fixed time
333                reply.setElement(1, 0x30);
334                break;
335            case NceMessage.READ_AUI4_CMD: // Read AUI 4 byte response
336                reply.setElement(0, 0xFF);   // fixed data for now
337                reply.setElement(1, 0xFF);   // fixed data for now
338                reply.setElement(2, 0x00);   // fixed data for now
339                reply.setElement(3, 0x00);   // fixed data for now
340                break;
341            case NceMessage.DUMMY_CMD:  // Dummy instruction
342                reply.setElement(0, NCE_OKAY);  // return ! CR LF
343                reply.setElement(1, 0x0D);
344                reply.setElement(2, 0x0A);
345                break;
346            case NceMessage.READ16_CMD:  // Read 16 bytes
347                readMemory(m, reply, 16);
348                break;
349            case NceMessage.READ_AUI2_CMD: // Read AUI 2 byte response
350                reply.setElement(0, 0x00);   // fixed data for now
351                reply.setElement(1, 0x00);   // fixed data for now
352                break;
353            case NceMessage.READ1_CMD:  // Read 1 bytes
354                readMemory(m, reply, 1);
355                break;
356            case NceMessage.WRITE1_CMD:  // Write 1 bytes
357                writeMemory(m, reply, 1, false);
358                break;
359            case NceMessage.WRITE2_CMD:  // Write 2 bytes
360                writeMemory(m, reply, 2, false);
361                break;
362            case NceMessage.WRITE4_CMD:  // Write 4 bytes
363                writeMemory(m, reply, 4, false);
364                break;
365            case NceMessage.WRITE8_CMD:  // Write 8 bytes
366                writeMemory(m, reply, 8, false);
367                break;
368            case NceMessage.WRITE_N_CMD:  // Write n bytes
369                writeMemory(m, reply, m.getElement(3), true);
370                break;
371            case NceMessage.SEND_ACC_SIG_MACRO_CMD:   // accessory command
372                accessoryCommand(m, reply);
373                break;
374            case NceMessage.READ_DIR_CV_CMD:
375            case NceMessage.READ_PAGED_CV_CMD:
376            case NceMessage.READ_REG_CMD:
377                reply.setElement(0, 123);   // dummy data
378                reply.setElement(1, NCE_OKAY);  // forces succeed
379                // Sample code to modify simulator response for testing purposes.
380                // Uncomment and modify as desired.
381//                int cvnum = (m.getElement(1) << 8) | (m.getElement(2));
382//                if (cvnum == 7) {
383//                    reply.setElement(0, 88);  // forces fail
384//                }
385//                if (cvnum == 8) {
386//                    reply.setElement(0, 48);  // forces Hornby
387//                }
388//                if (cvnum == 159) {
389//                    reply.setElement(0, 145);
390//                    reply.setElement(1, NCE_DATA_OUT_OF_RANGE);  // forces fail
391//                }
392                break;
393            default:
394                reply.setElement(0, NCE_OKAY);   // Nce okay reply!
395        }
396        return reply;
397    }
398
399    /**
400     * Write reply to output.
401     *
402     * @param r reply on message
403     */
404    private void writeReply(NceReply r) {
405        if (r == null) {
406            return;
407        }
408        for (int i = 0; i < r.getNumDataElements(); i++) {
409            try {
410                outpipe.writeByte((byte) r.getElement(i));
411            } catch (java.io.IOException ex) {
412            }
413        }
414        try {
415            outpipe.flush();
416        } catch (java.io.IOException ex) {
417        }
418    }
419
420    private final byte[] turnoutMemory = new byte[256];
421    private final byte[] macroMemory = new byte[256 * 20 + 16]; // and a little padding
422    private final byte[] consistMemory = new byte[256 * 6 + 16]; // and a little padding
423
424    /* Read NCE memory.  This implementation simulates reading the NCE
425     * command station memory.  There are three memory blocks that are
426     * supported, turnout status, macros, and consists.  The turnout status
427     * memory is 256 bytes and starts at memory address 0xEC00. The macro memory
428     * is 256*20 or 5120 bytes and starts at memory address 0xC800 (PH5 0x6000). The consist
429     * memory is 256*6 or 1536 bytes and starts at memory address 0xF500 (PH5 0x4E00).
430     *
431     */
432    private NceReply readMemory(NceMessage m, NceReply reply, int num) {
433        if (num > 16) {
434            log.error("Nce read memory command was greater than 16");
435            return null;
436        }
437        int nceMemoryAddress = getNceAddress(m);
438        if (nceMemoryAddress >= tc.csm.getAccyMemAddr() && nceMemoryAddress < tc.csm.getAccyMemAddr() + tc.csm.getAccyMemSize()) {
439            log.debug("Reading turnout memory: {}", Integer.toHexString(nceMemoryAddress));
440            int offset = m.getElement(2);
441            for (int i = 0; i < num; i++) {
442                reply.setElement(i, turnoutMemory[offset + i]);
443            }
444            return reply;
445        }
446        if (nceMemoryAddress >= tc.csm.getConsistHeadAddr() && nceMemoryAddress < tc.csm.getConsistHeadAddr() + tc.csm.getConsistMidSize()) {
447            log.debug("Reading consist memory: {}", Integer.toHexString(nceMemoryAddress));
448            int offset = nceMemoryAddress - tc.csm.getConsistHeadAddr();
449
450            for (int i = 0; i < num; i++) {
451                reply.setElement(i, consistMemory[offset + i]);
452            }
453            return reply;
454        }
455        if (nceMemoryAddress >= tc.csm.getMacroAddr() && nceMemoryAddress < tc.csm.getMacroAddr() + tc.csm.getMacroSize()) {
456            log.debug("Reading macro memory: {}", Integer.toHexString(nceMemoryAddress));
457            int offset = nceMemoryAddress - tc.csm.getMacroAddr();
458            log.debug("offset: {}", offset);
459            for (int i = 0; i < num; i++) {
460                reply.setElement(i, macroMemory[offset + i]);
461            }
462            return reply;
463        }
464        for (int i = 0; i < num; i++) {
465            reply.setElement(i, 0x00);   // default fixed data
466        }
467        return reply;
468    }
469
470    private NceReply writeMemory(NceMessage m, NceReply reply, int num, boolean skipbyte) {
471        if (num > 16) {
472            log.error("Nce write memory command was greater than 16");
473            return null;
474        }
475        int nceMemoryAddress = getNceAddress(m);
476        int byteDataBegins = 3;
477        if (skipbyte) {
478            byteDataBegins++;
479        }
480        if (nceMemoryAddress >= tc.csm.getMacroAddr() && nceMemoryAddress < tc.csm.getMacroAddr() + 256) {
481            log.debug("Writing turnout memory: {}", Integer.toHexString(nceMemoryAddress));
482            int offset = m.getElement(2);
483            for (int i = 0; i < num; i++) {
484                turnoutMemory[offset + i] = (byte) m.getElement(i + byteDataBegins);
485            }
486        }
487        if (nceMemoryAddress >= tc.csm.getMacroAddr() && nceMemoryAddress < tc.csm.getMacroAddr() + 256 * 6) {
488            log.debug("Writing consist memory: {}", Integer.toHexString(nceMemoryAddress));
489            int offset = nceMemoryAddress - tc.csm.getMacroAddr();
490            for (int i = 0; i < num; i++) {
491                consistMemory[offset + i] = (byte) m.getElement(i + byteDataBegins);
492            }
493        }
494        if (nceMemoryAddress >= tc.csm.getMacroAddr() && nceMemoryAddress < tc.csm.getMacroAddr() + 256 * 20) {
495            log.debug("Writing macro memory: {}", Integer.toHexString(nceMemoryAddress));
496            int offset = nceMemoryAddress - tc.csm.getMacroAddr();
497            log.debug("offset: {}", offset);
498            for (int i = 0; i < num; i++) {
499                macroMemory[offset + i] = (byte) m.getElement(i + byteDataBegins);
500            }
501        }
502        reply.setElement(0, NCE_OKAY);   // Nce okay reply!
503        return reply;
504    }
505
506    /**
507     * Extract item address from a message.
508     *
509     * @param m received message
510     * @return address from the message
511     */
512    private int getNceAddress(NceMessage m) {
513        int addr = m.getElement(1);
514        addr = addr * 256;
515        addr = addr + m.getElement(2);
516        return addr;
517    }
518
519    private NceReply accessoryCommand(NceMessage m, NceReply reply) {
520        if (m.getElement(3) == 0x03 || m.getElement(3) == 0x04) {  // 0x03 = close, 0x04 = throw
521            String operation = "close";
522            if (m.getElement(3) == 0x04) {
523                operation = "throw";
524            }
525            int nceAccessoryAddress = getNceAddress(m);
526            log.debug("Accessory command {} NT {}", operation, nceAccessoryAddress);
527            if (nceAccessoryAddress > 2044) {
528                log.error("Turnout address greater than 2044, address: {}", nceAccessoryAddress);
529                return null;
530            }
531            int bit = (nceAccessoryAddress - 1) & 0x07;
532            int setMask = 0x01;
533            for (int i = 0; i < bit; i++) {
534                setMask = setMask << 1;
535            }
536            int clearMask = 0x0FFF - setMask;
537            // log.debug("setMask: {} clearMask: {}", Integer.toHexString(setMask), Integer.toHexString(clearMask));
538            int offset = (nceAccessoryAddress - 1) >> 3;
539            int read = turnoutMemory[offset];
540            byte write = (byte) (read & clearMask & 0xFF);
541
542            if (operation.equals("close")) {
543                write = (byte) (write + setMask); // set bit if closed
544            }
545            turnoutMemory[offset] = write;
546            // log.debug("wrote: {}", Integer.toHexString(write));
547        }
548        reply.setElement(0, NCE_OKAY);   // Nce okay reply!
549        return reply;
550    }
551
552    private final static Logger log = LoggerFactory.getLogger(SimulatorAdapter.class);
553
554}