    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}