001package jmri.jmrix.sprog.simulator;
002
003import java.io.DataInputStream;
004import java.io.DataOutputStream;
005import java.io.IOException;
006import java.io.PipedInputStream;
007import java.io.PipedOutputStream;
008import jmri.jmrix.sprog.SprogConstants.SprogMode;
009import jmri.jmrix.sprog.SprogMessage;
010import jmri.jmrix.sprog.SprogPortController; // no special xSimulatorController
011import jmri.jmrix.sprog.SprogReply;
012import jmri.jmrix.sprog.SprogSystemConnectionMemo;
013import jmri.jmrix.sprog.SprogTrafficController;
014import jmri.util.ImmediatePipedOutputStream;
015import org.slf4j.Logger;
016import org.slf4j.LoggerFactory;
017
018/**
019 * Provide access to a simulated SPROG system.
020 * <p>
021 * Can be loaded as either a Programmer or Command Station.
022 * The SPROG SimulatorAdapter reacts to commands sent from the user interface
023 * with an appropriate reply message.
024 * <p>
025 * Based on jmri.jmrix.lenz.xnetsimulator.XNetSimulatorAdapter / DCCppSimulatorAdapter 2017
026 *
027 * @author Paul Bender, Copyright (C) 2009-2010
028 * @author Mark Underwood, Copyright (C) 2015
029 * @author Egbert Broerse, Copyright (C) 2018
030 */
031public class SimulatorAdapter extends SprogPortController implements Runnable {
032
033    // private control members
034    private Thread sourceThread;
035    private SprogTrafficController control;
036
037    private boolean outputBufferEmpty = true;
038    private boolean checkBuffer = true;
039    private SprogMode operatingMode = SprogMode.SERVICE;
040
041    // Simulator responses
042    String SPR_OK = "OK";
043    String SPR_NO = "No Ack";
044    String SPR_PR = "\nP> "; // prompt
045
046    public SimulatorAdapter() {
047        super(new SprogSystemConnectionMemo(SprogMode.SERVICE)); // use default user name
048        // starts as SERVICE mode (Programmer); may be set to OPS (Command Station) from connection option
049        setManufacturer(jmri.jmrix.sprog.SprogConnectionTypeList.SPROG);
050        this.getSystemConnectionMemo().setUserName(Bundle.getMessage("SprogSimulatorTitle"));
051        // create the traffic controller
052        control = new SprogTrafficController(this.getSystemConnectionMemo());
053        this.getSystemConnectionMemo().setSprogTrafficController(control);
054
055        options.put("NumSlots", // NOI18N
056                new Option(Bundle.getMessage("MakeLabel", Bundle.getMessage("NumSlotOptions")), // NOI18N
057                        new String[]{"16", "8", "32", "48", "64"}, true));
058
059        options.put("OperatingMode", // NOI18N
060                new Option(Bundle.getMessage("MakeLabel", Bundle.getMessage("SprogSimOption")), // NOI18N
061                        new String[]{Bundle.getMessage("SprogProgrammerTitle"),
062                                Bundle.getMessage("SprogCSTitle")}, true));
063    }
064
065    /**
066     * {@inheritDoc}
067     * Simulated input/output pipes.
068     */
069    @Override
070    public String openPort(String portName, String appName) {
071        try {
072            PipedOutputStream tempPipeI = new ImmediatePipedOutputStream();
073            log.debug("tempPipeI created");
074            pout = new DataOutputStream(tempPipeI);
075            inpipe = new DataInputStream(new PipedInputStream(tempPipeI));
076            log.debug("inpipe created {}", inpipe != null);
077            PipedOutputStream tempPipeO = new ImmediatePipedOutputStream();
078            outpipe = new DataOutputStream(tempPipeO);
079            pin = new DataInputStream(new PipedInputStream(tempPipeO));
080        } catch (java.io.IOException e) {
081            log.error("init (pipe): Exception: {}", e.toString());
082        }
083        opened = true;
084        return null; // indicates OK return
085    }
086
087    /**
088     * Set if the output buffer is empty or full. This should only be set to
089     * false by external processes.
090     *
091     * @param s true if output buffer is empty; false otherwise
092     */
093    synchronized public void setOutputBufferEmpty(boolean s) {
094        outputBufferEmpty = s;
095    }
096
097    /**
098     * Can the port accept additional characters? The state of CTS determines
099     * this, as there seems to be no way to check the number of queued bytes and
100     * buffer length. This might go false for short intervals, but it might also
101     * stick off if something goes wrong.
102     *
103     * @return true if port can accept additional characters; false otherwise
104     */
105    public boolean okToSend() {
106        if (checkBuffer) {
107            log.debug("Buffer Empty: {}", outputBufferEmpty);
108            return (outputBufferEmpty);
109        } else {
110            log.debug("No Flow Control or Buffer Check");
111            return (true);
112        }
113    }
114
115    /**
116     * Set up all of the other objects to operate with a Sprog Simulator.
117     */
118    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings( value = "SLF4J_FORMAT_SHOULD_BE_CONST",
119        justification = "passing exception text")
120    @Override
121    public void configure() {
122        // connect to the traffic controller
123        this.getSystemConnectionMemo().getSprogTrafficController().connectPort(this);
124
125        if (getOptionState("OperatingMode") != null && getOptionState("OperatingMode").equals(Bundle.getMessage("SprogProgrammerTitle"))) {
126            operatingMode = SprogMode.SERVICE;
127        } else { // default, also used after Locale change
128            operatingMode = SprogMode.OPS;
129        }
130        
131        String slots = getOptionState("NumSlots");
132        int numSlots;
133        try {
134            numSlots = Integer.parseInt(slots);
135        }
136        catch (NumberFormatException e) {
137            numSlots = 16;
138        }
139        
140        this.getSystemConnectionMemo().setSprogMode(operatingMode);         // first update mode in memo
141        this.getSystemConnectionMemo().configureCommandStation(numSlots);   // CS only if in OPS mode, memo will take care of that
142        this.getSystemConnectionMemo().configureManagers();                 // wait for mode to be correct
143
144        if (getOptionState("TrackPowerState") != null && getOptionState("TrackPowerState").equals(Bundle.getMessage("PowerStateOn"))) {
145            try {
146                this.getSystemConnectionMemo().getPowerManager().setPower(jmri.PowerManager.ON);
147            } catch (jmri.JmriException e) {
148                log.error(e.toString());
149            }
150        }
151
152        log.debug("SimulatorAdapter configure() with prefix = {}", this.getSystemConnectionMemo().getSystemPrefix());
153        // start the simulator
154        sourceThread = new Thread(this);
155        sourceThread.setName("SPROG Simulator");
156        sourceThread.setPriority(Thread.MIN_PRIORITY);
157        sourceThread.start();
158    }
159
160    /**
161     * {@inheritDoc}
162     */
163    @Override
164    public void connect() throws java.io.IOException {
165        log.debug("connect called");
166        super.connect();
167    }
168
169    // Base class methods for the SprogPortController simulated interface
170
171    /**
172     * {@inheritDoc}
173     */
174    @Override
175    public DataInputStream getInputStream() {
176        if (!opened || pin == null) {
177            log.error("getInputStream called before load(), stream not available");
178        }
179        log.debug("DataInputStream pin returned");
180        return pin;
181    }
182
183    /**
184     * {@inheritDoc}
185     */
186    @Override
187    public DataOutputStream getOutputStream() {
188        if (!opened || pout == null) {
189            log.error("getOutputStream called before load(), stream not available");
190        }
191        log.debug("DataOutputStream pout returned");
192        return pout;
193    }
194
195    /**
196     * {@inheritDoc}
197     */
198    @Override
199    public boolean status() {
200        return (pout != null && pin != null);
201    }
202
203    /**
204     * {@inheritDoc}
205     *
206     * @return null
207     */
208    @Override
209    public String[] validBaudRates() {
210        log.debug("validBaudRates should not have been invoked");
211        return new String[]{};
212    }
213
214    /**
215     * {@inheritDoc}
216     */
217    @Override
218    public int[] validBaudNumbers() {
219        return new int[]{};
220    }
221
222    @Override
223    public String getCurrentBaudRate() {
224        return "";
225    }
226
227    @Override
228    public String getCurrentPortName(){
229        return "";
230    }
231
232    @Override
233    public void run() { // start a new thread
234        // This thread has one task. It repeatedly reads from the input pipe
235        // and writes an appropriate response to the output pipe. This is the heart
236        // of the SPROG command station simulation.
237        log.info("SPROG Simulator Started");
238        while (true) {
239            try {
240                synchronized (this) {
241                    wait(50);
242                }
243            } catch (InterruptedException e) {
244                log.debug("interrupted, ending");
245                return;
246            }
247            SprogMessage m = readMessage();
248            SprogReply r;
249            if (log.isDebugEnabled()) {
250                StringBuffer buf = new StringBuffer();
251                buf.append("SPROG Simulator Thread received message: ");
252                if (m != null) {
253                    buf.append(m);
254                } else {
255                    buf.append("null message buffer");
256                }
257                //log.debug(buf.toString()); // generates a lot of output
258            }
259            if (m != null) {
260                r = generateReply(m);
261                writeReply(r);
262                log.debug("Simulator Thread sent Reply: \"{}\"", r);
263            }
264        }
265    }
266
267    /**
268     * Read one incoming message from the buffer
269     * and set outputBufferEmpty to true.
270     */
271    private SprogMessage readMessage() {
272        SprogMessage msg = null;
273        // log.debug("Simulator reading message");
274        try {
275            if (inpipe != null && inpipe.available() > 0) {
276                msg = loadChars();
277            }
278        } catch (java.io.IOException e) {
279            // should do something meaningful here.
280        }
281        setOutputBufferEmpty(true);
282        return (msg);
283    }
284
285    /**
286     * This is the heart of the simulation. It translates an
287     * incoming SprogMessage into an outgoing SprogReply.
288     *
289     * Based on SPROG information from A. Crosland.
290     * @see jmri.jmrix.sprog.SprogReply#value()
291     */
292    private SprogReply generateReply(SprogMessage msg) {
293        log.debug("Generate Reply to message type {} (string = {})", msg.toString().charAt(0), msg.toString());
294
295        SprogReply reply = new SprogReply();
296        int i = 0;
297        char command = msg.toString().charAt(0);
298        log.debug("Message type = {}", command);
299        switch (command) {
300
301            case 'I':
302                log.debug("CurrentQuery detected");
303                reply = new SprogReply("= h3E7\n"); // reply fictionary current (decimal 999mA)
304                break;
305
306            case 'C':
307            case 'V':
308                log.debug("Read/Write CV detected");
309                reply = new SprogReply("= h" + msg.toString().substring(2) + "\n"); // echo CV value (hex)
310                break;
311
312            case 'D':
313            case 'U':
314                log.debug("Read/Write CV with hint detected");
315                reply = new SprogReply("= h" + msg.toString().substring(2) + "\n"); // echo CV hint value (hex)
316                break;
317
318            case 'O':
319                log.debug("Send packet command detected");
320                reply = new SprogReply("= " + msg.toString().substring(2) + "\n"); // echo command (hex)
321                break;
322
323            case 'A':
324                log.debug("Address (open Throttle) command detected");
325                reply = new SprogReply(msg.toString().substring(2) + "\n"); // echo address (decimal)
326                break;
327
328            case '>':
329                log.debug("Set speed (Throttle) command detected");
330                reply = new SprogReply(msg.toString().substring(1) + "\n"); // echo speed (decimal)
331                break;
332
333            case '+':
334                log.debug("TRACK_POWER_ON detected");
335                //reply = new SprogReply(SPR_PR);
336                break;
337
338            case '-':
339                log.debug("TRACK_POWER_OFF detected");
340                //reply = new SprogReply(SPR_PR);
341                break;
342
343            case '?':
344                log.debug("Read_Sprog_Version detected");
345                String replyString = "\nSPROG II Ver 4.5\n";
346                reply = new SprogReply(replyString);
347                break;
348
349            case 'M':
350                log.debug("Mode Word detected");
351                reply = new SprogReply("P>M=h800\n"); // default mode reply
352                break;
353
354            case 'S':
355                log.debug("getStatus detected");
356                reply = new SprogReply("OK\n");
357                break;
358
359            case ' ':
360                log.debug("null command detected");
361                reply = new SprogReply("\n");
362                break;
363
364            default:
365                log.debug("non-reply message detected: {}", msg.toString());
366                reply = new SprogReply("!E\n"); // SPROG error reply
367        }
368        i = reply.toString().length();
369        reply.setElement(i++, 'P'); // add prompt to all replies
370        reply.setElement(i++, '>');
371        reply.setElement(i++, ' ');
372        log.debug("Reply generated = \"{}\"", reply.toString());
373        return reply;
374    }
375
376    /**
377     * Write reply to output.
378     * <p>
379     * Copied from jmri.jmrix.nce.simulator.SimulatorAdapter,
380     * adapted for {@link jmri.jmrix.sprog.SprogTrafficController#handleOneIncomingReply()}.
381     *
382     * @param r reply on message
383     */
384    private void writeReply(SprogReply r) {
385        if (r == null) {
386            return; // there is no reply to be sent
387        }
388        int len = r.getNumDataElements();
389        for (int i = 0; i < len; i++) {
390            try {
391                outpipe.writeByte((byte) r.getElement(i));
392                //log.debug("{} of {} bytes written to outpipe", i + 1, len);
393                if (pin.available() > 0) {
394                    control.handleOneIncomingReply();
395                }
396            } catch (java.io.IOException ex) {
397            }
398        }
399        try {
400            outpipe.flush();
401        } catch (java.io.IOException ex) {
402        }
403    }
404
405    /**
406     * Get characters from the input source.
407     * <p>
408     * Only used in the Receive thread.
409     *
410     * @return filled message, only when the message is complete.
411     * @throws IOException when presented by the input source.
412     */
413    private SprogMessage loadChars() throws java.io.IOException {
414        // code copied from EasyDcc/NCE Simulator
415        int nchars;
416        byte[] rcvBuffer = new byte[32];
417
418        nchars = inpipe.read(rcvBuffer, 0, 32);
419        //log.debug("new message received");
420        SprogMessage msg = new SprogMessage(nchars);
421
422        for (int i = 0; i < nchars; i++) {
423            msg.setElement(i, rcvBuffer[i] & 0xFF);
424        }
425        return msg;
426    }
427
428    // streams to share with user class
429    private DataOutputStream pout = null;    // this is provided to classes who want to write to us
430    private DataInputStream pin = null;      // this is provided to classes who want data from us
431    // internal ends of the pipes
432    private DataOutputStream outpipe = null; // feed pin
433    private DataInputStream inpipe = null;   // feed pout
434
435    private final static Logger log = LoggerFactory.getLogger(SimulatorAdapter.class);
436
437}