001package jmri.jmrix.rps.serial;
002
003import java.util.Arrays;
004import java.io.IOException;
005
006import jmri.InvokeOnGuiThread;
007import jmri.jmrix.rps.Distributor;
008import jmri.jmrix.rps.Engine;
009import jmri.jmrix.rps.Reading;
010import jmri.jmrix.rps.RpsSystemConnectionMemo;
011import org.apache.commons.csv.CSVFormat;
012import org.apache.commons.csv.CSVParser;
013import org.apache.commons.csv.CSVRecord;
014
015/**
016 * Implements SerialPortAdapter for the RPS system.
017 * <p>
018 * Unlike many other SerialPortAdapters, this also converts the input stream
019 * into Readings that can be passed to the Distributor.
020 * <p>
021 * This version expects that the "A" command will send back "DATA,," followed by
022 * a list of receivers numbers, and data lines will be "0,0,0,0": A value for
023 * each address up to the max receiver, even if some are missing (0 in that
024 * case)
025 *
026 * @author Bob Jacobsen Copyright (C) 2001, 2002, 2008
027 */
028public class SerialAdapter extends jmri.jmrix.AbstractSerialPortController {
029
030    public SerialAdapter() {
031        super(new RpsSystemConnectionMemo());
032        option1Name = "Protocol"; // NOI18N
033        options.put(option1Name, new Option(Bundle.getMessage("ProtocolVersionLabel"), validOptions1));
034        this.manufacturerName = jmri.jmrix.rps.RpsConnectionTypeList.NAC;
035    }
036
037    /**
038     * {@inheritDoc}
039     */
040    @Override
041    public RpsSystemConnectionMemo getSystemConnectionMemo() {
042        return (RpsSystemConnectionMemo) super.getSystemConnectionMemo();
043    }
044
045    /**
046     * Set up all of the other objects to operate.
047     */
048    @Override
049    public void configure() {
050        // Connect the control objects:
051        log.debug("configure() connecting RPS objects");
052        // connect an Engine to the Distributor
053        Engine e = Engine.instance();
054        Distributor.instance().addReadingListener(e);
055        // start the reader
056        readerThread = new Thread(new Reader());
057        readerThread.start();
058
059        this.getSystemConnectionMemo().configureManagers();
060    }
061
062    @Override
063    public synchronized String openPort(String portName, String appName) {
064
065        // get and open the primary port
066        currentSerialPort = activatePort(portName, log);
067        if (currentSerialPort == null) {
068            log.error("failed to connect RPS to {}", portName);
069            return Bundle.getMessage("SerialPortNotFound", portName);
070        }
071        log.info("Connecting RPS to {} {}", portName, currentSerialPort);
072        
073        // try to set it for communication via SerialDriver
074        // find the baud rate value, configure comm options
075        int baud = currentBaudNumber(mBaudRate);
076        setBaudRate(currentSerialPort, baud);
077        configureLeads(currentSerialPort, true, true);
078        setFlowControl(currentSerialPort, FlowControl.NONE);
079
080        // report status
081        reportPortStatus(log, portName);
082
083        opened = true;
084        
085        // capture streams
086        serialStream = getInputStream();
087        ostream = getOutputStream();
088
089        return null; // indicates OK return
090    }
091
092    java.io.DataInputStream serialStream;
093    java.io.OutputStream ostream;
094    
095    /**
096     * Send output bytes, e.g. characters controlling operation, with small
097     * delays between the characters. This is used to reduce overrrun problems.
098     * @param bytes Array of characters to be sent one at a time
099     */
100    synchronized void sendBytes(byte[] bytes) {
101        try {
102            for (int i = 0; i < bytes.length - 1; i++) {
103                ostream.write(bytes[i]);
104                wait(3);
105            }
106            final byte endbyte = bytes[bytes.length - 1];
107            ostream.write(endbyte);
108        } catch (java.io.IOException e) {
109            log.error("Exception on output: ", e);
110        } catch (java.lang.InterruptedException e) {
111            Thread.currentThread().interrupt(); // retain if needed later
112            log.error("Interrupted output: ", e);
113        }
114    }
115
116    // base class methods for the PortController interface
117    @Override
118    public boolean status() {
119        return opened;
120    }
121
122    /**
123     * {@inheritDoc}
124     */
125    @Override
126    public String[] validBaudRates() {
127        return Arrays.copyOf(validSpeeds, validSpeeds.length);
128    }
129
130    /**
131     * {@inheritDoc}
132     */
133    @Override
134    public int[] validBaudNumbers() {
135        return Arrays.copyOf(validSpeedValues, validSpeedValues.length);
136    }
137
138    protected String[] validSpeeds = new String[]{Bundle.getMessage("Baud115200")};
139    protected int[] validSpeedValues = new int[]{115200};
140
141    @Override
142    public int defaultBaudIndex() {
143        return 0;
144    }
145
146    String[] validOptions1 = new String[]{Bundle.getMessage("Version1Choice"), Bundle.getMessage("Version2Choice")};
147
148    /**
149     * Set the second port option.
150     */
151    @Override
152    public void configureOption1(String value) {
153        setOptionState(option1Name, value);
154        if (value.equals(validOptions1[0])) {
155            version = 1;
156        } else if (value.equals(validOptions1[1])) {
157            version = 2;
158        }
159    }
160
161    // private control members
162    int[] offsetArray = null;
163
164    // code for handling the input characters
165    Thread readerThread;
166
167    // flag for protocol version
168    int version = 1;
169
170    /**
171     * Internal class to handle the separate character-receive thread.
172     */
173    class Reader implements Runnable {
174
175        /**
176         * Handle incoming characters. This is a permanent loop, looking for
177         * input messages in character form on the stream connected to the
178         * PortController via <code>connectPort</code>. Terminates with the
179         * input stream breaking out of the try block.
180         */
181        @Override
182        public void run() {
183            // have to limit verbosity!
184
185            while (true) {   // loop permanently, stream close will exit via exception
186                try {
187                    handleIncomingData();
188                } catch (java.io.IOException e) {
189                    log.warn("run: Exception: ", e);
190                }
191            }
192        }
193
194        static final int maxMsg = 200;
195        StringBuffer msg;
196        String msgString;
197
198        void handleIncomingData() throws java.io.IOException {
199            // we sit in this until the message is complete, relying on
200            // threading to let other stuff happen
201
202            // Create output message
203            msg = new StringBuffer(maxMsg);
204            // message exists, now fill it
205            int i;
206            for (i = 0; i < maxMsg; i++) {
207                char char1;
208
209                synchronized (SerialAdapter.this) {
210                    char1 = (char) serialStream.readByte();
211                }
212
213                if (char1 == 13) {  // 13 is the CR at the end; done this
214                    // way to be coding-independent
215                    break;
216                }
217                // Strip off the CR and LF
218                if (char1 != 10) {
219                    msg.append(char1);
220                }
221            }
222
223            // create the String to display (as String has .equals)
224            msgString = msg.toString();
225            log.debug("Msg <{}>", msgString);
226
227            // return a notification via the queue to ensure end
228            Runnable r = new Runnable() {
229
230                // retain a copy of the message at startup
231                String msgForLater = msgString;
232
233                @Override
234                public void run() {
235                    nextLine(msgForLater);
236                }
237            };
238            javax.swing.SwingUtilities.invokeLater(r);
239        }
240
241    } // end class Reader
242
243    /**
244     * Handle a new line from the device.
245     * <p>
246     * This needs to execute on the Swing GUI thread. It forwards via the
247     * Distributor object.
248     *
249     * @param s The new message to distribute
250     */
251    @InvokeOnGuiThread
252    protected void nextLine(String s) {
253        // check for startup lines we ignore
254        if (s.length() < 5) {
255            return;
256        }
257        if (s.startsWith("DATA,,")) {
258            // skip reply to A
259            setReceivers(s);
260            return;
261        }
262
263        Reading r;
264        try {
265            r = makeReading(s);
266        } catch (IOException e) {
267            log.error("Exception formatting input line \"{}\": ", s, e);
268            // r = new Reading(-1, new double[]{-1, -1, -1, -1} );
269            // skip handling this line
270            return;
271        }
272
273        if (r == null) {
274            return;  // nothing useful
275        }
276        // forward
277        try {
278            Distributor.instance().submitReading(r);
279        } catch (Exception e) {
280            log.error("Exception forwarding reading: ", e);
281        }
282    }
283
284    /**
285     * Handle the message which lists the receiver numbers. Just makes an array
286     * of those, which is not actually used.
287     * @param s Input line
288     */
289    void setReceivers(String s) {
290        try {
291            // parse string
292            CSVParser c = CSVParser.parse(s, CSVFormat.DEFAULT);
293            CSVRecord r = c.getRecords().get(0);
294            c.close();
295
296            // first two are "Data, ," so are ignored.
297            // rest are receiver numbers
298            // Find how many
299            int n = r.size() - 2;
300            log.debug("Found {} receivers", n);
301
302            // find max receiver number
303            int max = Integer.parseInt(r.get(r.size() - 1));
304            log.debug("Highest receiver address is {}", max);
305
306            offsetArray = new int[n];
307            for (int i = 0; i < n; i++) {
308                offsetArray[i] = Integer.parseInt(r.get(i + 2));
309            }
310
311        } catch (IOException e) {
312            log.debug("Did not handle init message <{}> due to {}", s, e);
313        }
314    }
315
316    static private final int SKIPCOLS = 0; // used to skip DATA,TIME; was there a trailing "'"?
317    private boolean first = true;
318
319    /**
320     * Convert input line to Reading object.
321     * @param s The line of input
322     * @return A Reading object with content parsed from the input line
323     * @throws IOException from underlying I/O
324     */
325    Reading makeReading(String s) throws IOException {
326        if (first) {
327            log.info("RPS starts, using protocol version {}", version);
328            first = false;
329        }
330
331        if (version == 1) {
332            // parse string
333            CSVParser c = CSVParser.parse(s, CSVFormat.DEFAULT);
334            CSVRecord record = c.getRecords().get(0);
335            c.close();
336
337            // values are stored in 1-N of the output array; 0 not used
338            int count = record.size() - SKIPCOLS;
339            double[] vals = new double[count + 1];
340            for (int i = 1; i < count + 1; i++) {
341                vals[i] = Double.valueOf(record.get(i + SKIPCOLS - 1));
342            }
343
344            return new Reading(Engine.instance().getPolledID(), vals, s);
345        } else if (version == 2) {
346            // parse string
347            CSVParser c = CSVParser.parse(s, CSVFormat.DEFAULT);
348            CSVRecord record = c.getRecords().get(0);
349            c.close();
350
351            int count = (record.size() - 2) / 2;  // skip 'ADR, DAT,'
352            double[] vals = new double[Engine.instance().getMaxReceiverNumber() + 1]; // Receiver 2 goes in element 2
353            for (int i = 0; i < vals.length; i++) {
354                vals[i] = 0.0;
355            }
356            try {
357                for (int i = 0; i < count; i++) {  // i is zero-based count of input pairs
358                    int index = Integer.parseInt(record.get(2 + i * 2));  // index is receiver number
359                    // numbers are from one for valid receivers
360                    // the null message starts with index zero
361                    if (index < 0) {
362                        continue;
363                    }
364                    if (index >= vals.length) { // data for undefined Receiver
365                        log.warn("Data from unexpected receiver {}, creating receiver", index);
366                        Engine.instance().setMaxReceiverNumber(index + 1);
367                        //
368                        // Originally, we made vals[] longer if we got
369                        // a response from an unexpected receiver.
370                        // This caused terrible trouble at Kesen's layout,
371                        // so was commented-out here.
372                        //
373                        //double[] temp = new double[index+1];
374                        //for (int j = 0; j<vals.length; j++) temp[j] = vals[j];
375                        //vals = temp;
376                    }
377                    if (index < vals.length) {
378                        vals[index] = Double.valueOf(record.get(2 + i * 2 + 1));
379                    }
380                }
381            } catch (NumberFormatException e) {
382                log.warn("Exception handling input.", e);
383                return null;
384            }
385            return new Reading(Engine.instance().getPolledID(), vals, s);
386        } else {
387            log.error("can't handle version {}", version);
388            return null;
389        }
390    }
391
392    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(SerialAdapter.class);
393
394}