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