001/** 002 * Copyright (c) 2014 Digi International Inc., 003 * All rights not expressly granted are reserved. 004 * 005 * This Source Code Form is subject to the terms of the Mozilla Public 006 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 007 * You can obtain one at http://mozilla.org/MPL/2.0/. 008 * 009 * Digi International Inc. 11001 Bren Road East, Minnetonka, MN 55343 010 * ======================================================================= 011 */ 012package com.digi.xbee.api.packet; 013 014import java.io.ByteArrayInputStream; 015import java.io.IOException; 016import java.io.InputStream; 017import java.util.Date; 018 019import com.digi.xbee.api.exceptions.InvalidPacketException; 020import com.digi.xbee.api.models.SpecialByte; 021import com.digi.xbee.api.models.OperatingMode; 022import com.digi.xbee.api.packet.common.ATCommandPacket; 023import com.digi.xbee.api.packet.common.ATCommandQueuePacket; 024import com.digi.xbee.api.packet.common.ATCommandResponsePacket; 025import com.digi.xbee.api.packet.common.IODataSampleRxIndicatorPacket; 026import com.digi.xbee.api.packet.common.ModemStatusPacket; 027import com.digi.xbee.api.packet.common.ReceivePacket; 028import com.digi.xbee.api.packet.common.RemoteATCommandPacket; 029import com.digi.xbee.api.packet.common.RemoteATCommandResponsePacket; 030import com.digi.xbee.api.packet.common.TransmitPacket; 031import com.digi.xbee.api.packet.common.TransmitStatusPacket; 032import com.digi.xbee.api.packet.raw.RX16IOPacket; 033import com.digi.xbee.api.packet.raw.RX16Packet; 034import com.digi.xbee.api.packet.raw.RX64IOPacket; 035import com.digi.xbee.api.packet.raw.RX64Packet; 036import com.digi.xbee.api.packet.raw.TX16Packet; 037import com.digi.xbee.api.packet.raw.TX64Packet; 038import com.digi.xbee.api.packet.raw.TXStatusPacket; 039import com.digi.xbee.api.utils.HexUtils; 040 041/** 042 * This class reads and parses XBee packets from the input stream returning 043 * a generic {@code XBeePacket} which can be casted later to the corresponding 044 * high level specific API packet. 045 * 046 * <p>All the API and API2 logic is already included so all packet reads are 047 * independent of the XBee operating mode.</p> 048 * 049 * <p>Two API modes are supported and both can be enabled using the {@code AP} 050 * (API Enable) command: 051 * 052 * <ul> 053 * <li><b>API1 - API Without Escapes</b> 054 * <p>The data frame structure is defined as follows:</p> 055 * 056 * <pre> 057 * {@code 058 * Start Delimiter Length Frame Data Checksum 059 * (Byte 1) (Bytes 2-3) (Bytes 4-n) (Byte n + 1) 060 * +----------------+ +-------------------+ +--------------------------- + +----------------+ 061 * | 0x7E | | MSB | LSB | | API-specific Structure | | 1 Byte | 062 * +----------------+ +-------------------+ +----------------------------+ +----------------+ 063 * MSB = Most Significant Byte, LSB = Least Significant Byte 064 * } 065 * </pre> 066 * </li> 067 * 068 * <li><b>API2 - API With Escapes</b> 069 * <p>The data frame structure is defined as follows:</p> 070 * 071 * <pre> 072 * {@code 073 * Start Delimiter Length Frame Data Checksum 074 * (Byte 1) (Bytes 2-3) (Bytes 4-n) (Byte n + 1) 075 * +----------------+ +-------------------+ +--------------------------- + +----------------+ 076 * | 0x7E | | MSB | LSB | | API-specific Structure | | 1 Byte | 077 * +----------------+ +-------------------+ +----------------------------+ +----------------+ 078 * \___________________________________ _________________________________/ 079 * \/ 080 * Characters Escaped If Needed 081 * 082 * MSB = Most Significant Byte, LSB = Least Significant Byte 083 * } 084 * </pre> 085 * 086 * <p>When sending or receiving an API2 frame, specific data values must be 087 * escaped (flagged) so they do not interfere with the data frame sequencing. 088 * To escape an interfering data byte, the byte {@code 0x7D} is inserted before 089 * the byte to be escaped XOR'd with {@code 0x20}.</p> 090 * 091 * <p>The data bytes that need to be escaped:</p> 092 * <ul> 093 * <li>{@code 0x7E} - Frame Delimiter ({@link SpecialByte#HEADER_BYTE})</li> 094 * <li>{@code 0x7D} - Escape ({@link SpecialByte#ESCAPE_BYTE})</li> 095 * <li>{@code 0x11} - XON ({@link SpecialByte#XON_BYTE})</li> 096 * <li>{@code 0x13} - XOFF ({@link SpecialByte#XOFF_BYTE})</li> 097 * </ul> 098 * 099 * </li> 100 * </ul> 101 * 102 * <p>The <b>length</b> field has a two-byte value that specifies the number of 103 * bytes that will be contained in the frame data field. It does not include the 104 * checksum field.</p> 105 * 106 * <p>The <b>frame data</b> forms an API-specific structure as follows:</p> 107 * 108 * <pre> 109 * {@code 110 * Start Delimiter Length Frame Data Checksum 111 * (Byte 1) (Bytes 2-3) (Bytes 4-n) (Byte n + 1) 112 * +----------------+ +-------------------+ +--------------------------- + +----------------+ 113 * | 0x7E | | MSB | LSB | | API-specific Structure | | 1 Byte | 114 * +----------------+ +-------------------+ +----------------------------+ +----------------+ 115 * / \ 116 * / API Identifier Identifier specific data \ 117 * +------------------+ +------------------------------+ 118 * | cmdID | | cmdData | 119 * +------------------+ +------------------------------+ 120 * } 121 * </pre> 122 * 123 * <p>The {@code cmdID} frame (API-identifier) indicates which API messages 124 * will be contained in the {@code cmdData} frame (Identifier-specific data). 125 * </p> 126 * 127 * <p>To test data integrity, a <b>checksum</b> is calculated and verified on 128 * non-escaped data.</p> 129 * 130 * @see APIFrameType 131 * @see XBeePacket 132 * @see com.digi.xbee.api.models.OperatingMode 133 */ 134public class XBeePacketParser { 135 136 /** 137 * Parses the bytes from the given input stream depending on the provided 138 * operating mode and returns the API packet. 139 * 140 * <p>The operating mode must be {@link OperatingMode#API} or 141 * {@link OperatingMode#API_ESCAPE}.</p> 142 * 143 * @param inputStream Input stream to read bytes from. 144 * @param mode XBee device operating mode. 145 * 146 * @return Parsed packet from the input stream. 147 * 148 * @throws IllegalArgumentException if {@code mode != OperatingMode.API } and 149 * if {@code mode != OperatingMode.API_ESCAPE}. 150 * @throws InvalidPacketException if there is not enough data in the stream or 151 * if there is an error verifying the checksum or 152 * if the payload is invalid for the specified frame type. 153 * @throws NullPointerException if {@code inputStream == null} or 154 * if {@code mode == null}. 155 * 156 * @see XBeePacket 157 * @see com.digi.xbee.api.models.OperatingMode#API 158 * @see com.digi.xbee.api.models.OperatingMode#API_ESCAPE 159 */ 160 public XBeePacket parsePacket(InputStream inputStream, OperatingMode mode) throws InvalidPacketException { 161 if (inputStream == null) 162 throw new NullPointerException("Input stream cannot be null."); 163 164 if (mode == null) 165 throw new NullPointerException("Operating mode cannot be null."); 166 167 if (mode != OperatingMode.API && mode != OperatingMode.API_ESCAPE) 168 throw new IllegalArgumentException("Operating mode must be API or API Escaped."); 169 170 try { 171 // Read packet size. 172 int hSize = readByte(inputStream, mode); 173 int lSize = readByte(inputStream, mode); 174 int length = hSize << 8 | lSize; 175 176 // Read the payload. 177 byte[] payload = readBytes(inputStream, mode, length); 178 179 // Calculate the expected checksum. 180 XBeeChecksum checksum = new XBeeChecksum(); 181 checksum.add(payload); 182 byte expectedChecksum = (byte)(checksum.generate() & 0xFF); 183 184 // Read checksum from the input stream. 185 byte readChecksum = (byte)(readByte(inputStream, mode) & 0xFF); 186 187 // Verify the checksum of the read bytes. 188 if (readChecksum != expectedChecksum) 189 throw new InvalidPacketException("Invalid checksum (expected 0x" 190 + HexUtils.byteToHexString(expectedChecksum) + ")."); 191 192 return parsePayload(payload); 193 194 } catch (IOException e) { 195 throw new InvalidPacketException("Error parsing packet: " + e.getMessage(), e); 196 } 197 } 198 199 /** 200 * Parses the bytes from the given array depending on the provided operating 201 * mode and returns the API packet. 202 * 203 * <p>The operating mode must be {@link OperatingMode#API} or 204 * {@link OperatingMode#API_ESCAPE}.</p> 205 * 206 * @param packetByteArray Byte array with the complete frame, starting from 207 * the header and ending in the checksum. 208 * @param mode XBee device operating mode. 209 * 210 * @return Parsed packet from the given byte array. 211 * 212 * @throws InvalidPacketException if there is not enough data in the array or 213 * if there is an error verifying the checksum or 214 * if the payload is invalid for the specified frame type. 215 * @throws IllegalArgumentException if {@code mode != OperatingMode.API } and 216 * if {@code mode != OperatingMode.API_ESCAPE}. 217 * @throws NullPointerException if {@code packetByteArray == null} or 218 * if {@code mode == null}. 219 * 220 * @see XBeePacket 221 * @see com.digi.xbee.api.models.OperatingMode#API 222 * @see com.digi.xbee.api.models.OperatingMode#API_ESCAPE 223 */ 224 public XBeePacket parsePacket(byte[] packetByteArray, OperatingMode mode) throws InvalidPacketException { 225 if (packetByteArray == null) 226 throw new NullPointerException("Packet byte array cannot be null."); 227 228 if (mode == null) 229 throw new NullPointerException("Operating mode cannot be null."); 230 231 if (mode != OperatingMode.API && mode != OperatingMode.API_ESCAPE) 232 throw new IllegalArgumentException("Operating mode must be API or API Escaped."); 233 234 // Check the byte array has at least 4 bytes. 235 if (packetByteArray.length < 4) 236 throw new InvalidPacketException("Error parsing packet: Incomplete packet."); 237 238 // Check the header of the frame. 239 if ((packetByteArray[0] & 0xFF) != SpecialByte.HEADER_BYTE.getValue()) 240 throw new InvalidPacketException("Invalid start delimiter (expected 0x" 241 + HexUtils.byteToHexString((byte)SpecialByte.HEADER_BYTE.getValue()) + ")."); 242 243 return parsePacket(new ByteArrayInputStream(packetByteArray, 1, packetByteArray.length - 1), mode); 244 } 245 246 /** 247 * Parses the given API payload to get the right API packet, depending 248 * on its API type ({@code payload[0]}). 249 * 250 * @param payload The payload of the API frame. 251 * 252 * @return The corresponding API packet or {@code UnknownXBeePacket} if 253 * the frame API type is unknown. 254 * 255 * @throws InvalidPacketException if the payload is invalid for the 256 * specified frame type. 257 * 258 * @see APIFrameType 259 * @see XBeePacket 260 */ 261 private XBeePacket parsePayload(byte[] payload) throws InvalidPacketException { 262 // Get the API frame type. 263 APIFrameType apiType = APIFrameType.get(payload[0] & 0xFF); 264 265 if (apiType == null) 266 // Create unknown packet. 267 return UnknownXBeePacket.createPacket(payload); 268 269 // Parse API payload depending on API ID. 270 XBeePacket packet = null; 271 switch (apiType) { 272 case TX_64: 273 packet = TX64Packet.createPacket(payload); 274 break; 275 case TX_16: 276 packet = TX16Packet.createPacket(payload); 277 break; 278 case AT_COMMAND: 279 packet = ATCommandPacket.createPacket(payload); 280 break; 281 case AT_COMMAND_QUEUE: 282 packet = ATCommandQueuePacket.createPacket(payload); 283 break; 284 case TRANSMIT_REQUEST: 285 packet = TransmitPacket.createPacket(payload); 286 break; 287 case REMOTE_AT_COMMAND_REQUEST: 288 packet = RemoteATCommandPacket.createPacket(payload); 289 break; 290 case RX_64: 291 packet = RX64Packet.createPacket(payload); 292 break; 293 case RX_16: 294 packet = RX16Packet.createPacket(payload); 295 break; 296 case RX_IO_64: 297 packet = RX64IOPacket.createPacket(payload); 298 break; 299 case RX_IO_16: 300 packet = RX16IOPacket.createPacket(payload); 301 break; 302 case AT_COMMAND_RESPONSE: 303 packet = ATCommandResponsePacket.createPacket(payload); 304 break; 305 case TX_STATUS: 306 packet = TXStatusPacket.createPacket(payload); 307 break; 308 case MODEM_STATUS: 309 packet = ModemStatusPacket.createPacket(payload); 310 break; 311 case TRANSMIT_STATUS: 312 packet = TransmitStatusPacket.createPacket(payload); 313 break; 314 case RECEIVE_PACKET: 315 packet = ReceivePacket.createPacket(payload); 316 break; 317 case IO_DATA_SAMPLE_RX_INDICATOR: 318 packet = IODataSampleRxIndicatorPacket.createPacket(payload); 319 break; 320 case REMOTE_AT_COMMAND_RESPONSE: 321 packet = RemoteATCommandResponsePacket.createPacket(payload); 322 break; 323 case GENERIC: 324 default: 325 packet = GenericXBeePacket.createPacket(payload); 326 } 327 return packet; 328 } 329 330 /** 331 * Reads one byte from the input stream. 332 * 333 * <p>This operation checks several things like the working mode in order 334 * to consider escaped bytes.</p> 335 * 336 * @param inputStream Input stream to read bytes from. 337 * @param mode XBee device working mode. 338 * 339 * @return The read byte. 340 * 341 * @throws InvalidPacketException if there is not enough data in the stream or 342 * if there is an error verifying the checksum. 343 * @throws IOException if the first byte cannot be read for any reason other than end of file, or 344 * if the input stream has been closed, or 345 * if some other I/O error occurs. 346 */ 347 private int readByte(InputStream inputStream, OperatingMode mode) throws InvalidPacketException, IOException { 348 int timeout = 300; 349 350 int b = readByteFrom(inputStream, timeout); 351 352 if (b == -1) 353 throw new InvalidPacketException("Error parsing packet: Incomplete packet."); 354 355 /* Process the byte for API1. */ 356 357 if (mode == OperatingMode.API) 358 return b; 359 360 /* Process the byte for API2. */ 361 362 // Check if the byte is special. 363 if (!SpecialByte.isSpecialByte(b)) 364 return b; 365 366 // Check if the byte is ESCAPE. 367 if (b == SpecialByte.ESCAPE_BYTE.getValue()) { 368 // Read next byte and escape it. 369 b = readByteFrom(inputStream, timeout); 370 371 if (b == -1) 372 throw new InvalidPacketException("Error parsing packet: Incomplete packet."); 373 374 b ^= 0x20; 375 } else 376 // If the byte is not a escape there is a special byte not escaped. 377 throw new InvalidPacketException("Special byte not escaped: 0x" + HexUtils.byteToHexString((byte)(b & 0xFF)) + "."); 378 379 return b; 380 } 381 382 /** 383 * Reads the given amount of bytes from the input stream. 384 * 385 * <p>This operation checks several things like the working mode in order 386 * to consider escaped bytes.</p> 387 * 388 * @param inputStream Input stream to read bytes from. 389 * @param mode XBee device working mode. 390 * @param numBytes Number of bytes to read. 391 * 392 * @return The read byte array. 393 * 394 * @throws IOException if the first byte cannot be read for any reason other than end of file, or 395 * if the input stream has been closed, or 396 * if some other I/O error occurs. 397 * @throws InvalidPacketException if there is not enough data in the stream or 398 * if there is an error verifying the checksum. 399 */ 400 private byte[] readBytes(InputStream inputStream, OperatingMode mode, int numBytes) throws IOException, InvalidPacketException { 401 byte[] data = new byte[numBytes]; 402 403 for (int i = 0; i < numBytes; i++) 404 data[i] = (byte)readByte(inputStream, mode); 405 406 return data; 407 } 408 409 /** 410 * Reads a byte from the given input stream. 411 * 412 * @param inputStream The input stream to read the byte. 413 * @param timeout Timeout to wait for a byte in the input stream 414 * in milliseconds. 415 * 416 * @return The read byte or {@code -1} if the timeout expires or the end 417 * of the stream is reached. 418 * 419 * @throws IOException if an I/O errors occurs while reading the byte. 420 */ 421 private int readByteFrom(InputStream inputStream, int timeout) throws IOException { 422 long deadline = new Date().getTime() + timeout; 423 424 int b = inputStream.read(); 425 // Let's try again if the byte is -1. 426 while (b == -1 && new Date().getTime() < deadline) { 427 b = inputStream.read(); 428 try { 429 Thread.sleep(10); 430 } catch (InterruptedException e) {} 431 } 432 433 return b; 434 } 435}