001package jmri.jmrix.can.adapters.gridconnect;
002
003import jmri.jmrix.AbstractMRReply;
004import jmri.jmrix.can.CanReply;
005import org.slf4j.Logger;
006import org.slf4j.LoggerFactory;
007
008/**
009 * Class for replies in a GridConnect based message/reply protocol.
010 * <p>
011 * The GridConnect protocol encodes messages as an ASCII string of up to 24
012 * characters of the form: :ShhhhNd0d1d2d3d4d5d6d7;
013 * <p>
014 * hhhh is the two byte (11
015 * useful bits) header The S indicates a standard CAN frame
016 * :XhhhhhhhhNd0d1d2d3d4d5d6d7; The X indicates an extended CAN frame N or R
017 * indicates a normal or remote frame, in position 6 or 10 d0 - d7 are the (up
018 * to) 8 data bytes
019 *
020 * @author Andrew Crosland Copyright (C) 2008, 2009
021 * @author Bob Jacobsen Copyright (C) 2008
022 */
023public class GridConnectReply extends AbstractMRReply {
024
025    // The longest valid frame, an extended frame with 8 data bytes,
026    // :X12345678N0102030405060708; is 28 characters. Its ';' is not stored,
027    // but all of its data is, and the ';' left in the stream is skipped while
028    // looking for the ':' of the next frame.
029    static final int MAXLEN = 27;
030
031    // Maximum number of data bytes in a CAN frame
032    private static final int MAX_DATA_BYTES = 8;
033
034    /**
035     * Creates a new instance of GridConnectReply.
036     */
037    public GridConnectReply() {
038        _nDataChars = 0;
039        _dataChars = new int[MAXLEN];
040    }
041
042    /**
043     * Creates a new GridConnectReply from String.
044     * @param s String to use as basis for the GCReply.
045     */
046    public GridConnectReply(String s) {
047        _nDataChars = s.length();
048        for (int i = 0; i < s.length(); i++) {
049            _dataChars[i] = s.charAt(i);
050        }
051    }
052
053    /**
054     * Create a CanReply from a GridConnectReply.
055     * @return new CanReply Outgoing message.
056     */
057    public CanReply createReply() {
058        CanReply ret = new CanReply();
059
060        log.debug("createReply converts from {}", this);
061
062        // basic checks drop out the frame
063        if (!basicFormatCheck()) {
064            ret.setHeader(0);
065            ret.setNumDataElements(0);
066            return ret;
067        }
068
069        // Is the header terminated by N or R, and is the data valid ?
070        // A corrupt frame, eg. two frames merged after a lost ';', is dropped
071        // rather than being passed on with incorrect content.
072        getHeader(); // sets _RTRoffset
073        if (!dataFormatCheck()) {
074            log.warn("Invalid GridConnect frame dropped: {}", this);
075            ret.setHeader(0);
076            ret.setNumDataElements(0);
077            return ret;
078        }
079
080        // Is it an Extended frame?
081        if (isExtended()) {
082            ret.setExtended(true);
083        }
084
085        // Copy the header
086        ret.setHeader(getHeader());
087
088        // Is it an RTR frame?
089        if (isRtr()) {
090            ret.setRtr(true);
091        }
092
093        // Get the data
094        for (int i = 0; i < getNumBytes(); i++) {
095            ret.setElement(i, getByte(i));
096        }
097        ret.setNumDataElements(getNumBytes());
098        log.debug("createReply converted to {}", ret);
099        return ret;
100    }
101
102    /**
103     * Check if this GCReply contains an Extended or Standard flag.
104     * @return true if contains a flag, else false.
105     */
106    protected boolean basicFormatCheck() {
107        return !((getElement(1) != 'X') && (getElement(1) != 'S'));
108    }
109
110    /**
111     * Check the header is terminated by 'N' or 'R', and the data section is an
112     * even number of hex digits, no more than 8 bytes, and nothing follows it
113     * except an optional ';'.
114     * <p>
115     * Must be called after getHeader(), which locates the 'N' or 'R'.
116     * @return true if the frame is valid, else false.
117     */
118    protected boolean dataFormatCheck() {
119        if ((_RTRoffset < 2) || (_RTRoffset >= _nDataChars)
120                || ((_dataChars[_RTRoffset] != 'N') && (_dataChars[_RTRoffset] != 'R'))) {
121            return false; // no N or R following the header
122        }
123        // The header is 1 to 4 (standard) or 1 to 8 (extended) hex digits
124        // between the 'S' or 'X' and the 'N' or 'R'.
125        int headerDigits = _RTRoffset - 2;
126        if ((headerDigits < 1) || (headerDigits > (isExtended() ? 8 : 4))) {
127            return false;
128        }
129        for (int i = 2; i < _RTRoffset; i++) {
130            if (!isHexDigit(_dataChars[i])) {
131                return false;
132            }
133        }
134        int nChars = numDataChars();
135        if ((nChars < 0) || (nChars % 2 != 0) || (nChars > (MAX_DATA_BYTES * 2))) {
136            return false;
137        }
138        int start = _RTRoffset + 1;
139        for (int i = start; i < (start + nChars); i++) {
140            if (!isHexDigit(_dataChars[i])) {
141                return false;
142            }
143        }
144        return true;
145    }
146
147    /**
148     * Get the number of data characters, ie. following the 'N' or 'R', excluding
149     * a trailing ';'.
150     * @return number of data characters
151     */
152    private int numDataChars() {
153        int end = _nDataChars;
154        if ((end > 0) && (_dataChars[end - 1] == ';')) {
155            end--;
156        }
157        return end - (_RTRoffset + 1);
158    }
159
160    private static boolean isHexDigit(int b) {
161        return ((b >= '0') && (b <= '9')) || ((b >= 'A') && (b <= 'F')) || ((b >= 'a') && (b <= 'f'));
162    }
163
164    /**
165     * {@inheritDoc}
166     */
167    @Override
168    protected int skipPrefix(int index) {
169        while (_dataChars[index] == ':') {
170            index++;
171        }
172        return index;
173    }
174
175    /**
176     * {@inheritDoc}
177     */
178    @Override
179    public int getNumDataElements() {
180        return _nDataChars;
181    }
182
183    /**
184     * Set Number of Data Elements.
185     * Max. length set by the MAXLEN constant.
186     * @param n Number Elements.
187     */
188    public void setNumDataElements(int n) {
189        _nDataChars = (n <= MAXLEN) ? n : MAXLEN;
190    }
191
192    /**
193     * {@inheritDoc}
194     */
195    @Override
196    public int getElement(int n) {
197        return _dataChars[n];
198    }
199
200    /**
201     * {@inheritDoc}
202     */
203    @Override
204    public void setElement(int n, int v) {
205        if (n < MAXLEN) {
206            _dataChars[n] = v;
207            _nDataChars = Math.max(_nDataChars, n + 1);
208        }
209    }
210
211    /**
212     * Get if the GridConnectReply is Extended.
213     * @return true if extended, else false.
214     */
215    public boolean isExtended() {
216        return (getElement(1) == 'X');
217    }
218
219    /**
220     * Get if the GridConnectReply is RtR.
221     * @return true if RtR, else false.
222     */
223    public boolean isRtr() {
224        return (getElement(_RTRoffset) == 'R');
225    }
226
227    /**
228     * {@inheritDoc}
229     */
230    @Override
231    public int maxSize() {
232        return MAXLEN;
233    }
234
235    /**
236     * Set the GridConnectReply data by Array.
237     * @param d data array.
238     */
239    public void setData(int[] d) {
240        int len = (d.length <= MAXLEN) ? d.length : MAXLEN;
241        System.arraycopy(d, 0, _dataChars, 0, len);
242    }
243
244    // pointer to the N or R character
245    int _RTRoffset = -1;
246
247    /**
248     * Get the CAN header by using chars from 2 to up to 9.
249     * <p>
250     * Right justify standard headers that had 4 digits.
251     *
252     * @return the CAN header as an int
253     */
254    public int getHeader() {
255        int val = 0;
256        for (int i = 2; i <= 10; i++) {
257            _RTRoffset = i;
258            if (_dataChars[i] == 'N') {
259                break;
260            }
261            if (_dataChars[i] == 'R') {
262                break;
263            }
264            val = val * 16 + getHexDigit(i);
265        }
266        return val;
267    }
268
269    /**
270     * Get the number of data bytes.
271     * @return number of bytes in reply.
272     */
273    public int getNumBytes() {
274        // subtract framing and ID bytes, etc and each byte is two ASCII hex digits
275        int n = numDataChars() / 2;
276        return Math.max(0, Math.min(n, MAX_DATA_BYTES)); // never more than a CAN frame can hold
277    }
278
279    /**
280     * Get a hex data byte from the message.
281     * <p>
282     * Data bytes are encoded as two ASCII hex digits starting at byte 7 of the
283     * message.
284     *
285     * @param b The byte offset (0 - 7)
286     * @return The value
287     */
288    public int getByte(int b) {
289        if ((b >= 0) && (b <= 7)) {
290            int index = b * 2 + _RTRoffset + 1;
291            int hi = getHexDigit(index++);
292            int lo = getHexDigit(index);
293            if ((hi < 16) && (lo < 16)) {
294                return (hi * 16 + lo);
295            }
296        }
297        return 0;
298    }
299
300    // Get a single hex digit. returns 0 if digit is invalid
301    private int getHexDigit(int index) {
302        int b = _dataChars[index];
303        if ((b >= '0') && (b <= '9')) {
304            b = b - '0';
305        } else if ((b >= 'A') && (b <= 'F')) {
306            b = b - 'A' + 10;
307        } else if ((b >= 'a') && (b <= 'f')) {
308            b = b - 'a' + 10;
309        } else {
310            b = 0;
311        }
312        return (byte) b;
313    }
314
315    private static final Logger log = LoggerFactory.getLogger(GridConnectReply.class);
316}
317
318