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