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.ByteArrayOutputStream;
016import java.util.LinkedHashMap;
017
018import com.digi.xbee.api.exceptions.InvalidPacketException;
019import com.digi.xbee.api.models.SpecialByte;
020import com.digi.xbee.api.models.OperatingMode;
021import com.digi.xbee.api.utils.ByteUtils;
022import com.digi.xbee.api.utils.HexUtils;
023
024/**
025 * This abstract class represents the basic structure of an XBee packet.
026 * 
027 * <p>Derived classes should implement their own payload generation depending 
028 * on their type.</p>
029 * 
030 * <p>Generic actions like checksum compute or packet length calculation is 
031 * performed here.</p>
032 */
033public abstract class XBeePacket {
034
035        // Variables.
036        private XBeeChecksum checksum;
037        
038        /**
039         * Class constructor. Instantiates a new {@code XBeePacket} object.
040         */
041        protected XBeePacket() {
042                checksum = new XBeeChecksum();
043        }
044
045        /**
046         * Generates the XBee packet byte array. 
047         * 
048         * <p>Use only while working in API mode 1. If API mode is 2, use 
049         * {@link #generateByteArrayEscaped()}.</p>
050         * 
051         * @return The XBee packet byte array.
052         * 
053         * @see #generateByteArrayEscaped()
054         */
055        public byte[] generateByteArray() {
056                checksum.reset();
057                byte[] packetData = getPacketData();
058                ByteArrayOutputStream os = new ByteArrayOutputStream();
059                os.write(SpecialByte.HEADER_BYTE.getValue());
060                if (packetData != null) {
061                        byte[] length = ByteUtils.shortToByteArray((short)packetData.length);
062                        int msb = length[0];
063                        int lsb = length[1];
064                        os.write(msb);
065                        os.write(lsb);
066                        for (int i = 0; i < packetData.length; i++) {
067                                checksum.add(packetData[i]);
068                                os.write(packetData[i]);
069                        }
070                } else {
071                        os.write(0);
072                        os.write(0);
073                }
074                os.write((byte)checksum.generate() & 0xFF);
075                return os.toByteArray();
076        }
077
078        /**
079         * Generates the XBee packet byte array escaping the special bytes.
080         * 
081         * <p>Use only while working in API mode 2. If API mode is 1 use 
082         * {@link #generateByteArray()}.</p>
083         * 
084         * @return The XBee packet byte array with escaped characters.
085         * 
086         * @see #generateByteArray()
087         */
088        public byte[] generateByteArrayEscaped() {
089                byte[] unescapedArray = generateByteArray();
090                ByteArrayOutputStream os = new ByteArrayOutputStream();
091                // Write header byte and do not escape it.
092                os.write(SpecialByte.HEADER_BYTE.getValue());
093                for (int i = 1; i < unescapedArray.length; i++) {
094                        // Start at 1 to avoid escaping header byte.
095                        if (SpecialByte.isSpecialByte(unescapedArray[i])) {
096                                os.write(SpecialByte.ESCAPE_BYTE.getValue());
097                                SpecialByte specialByte = SpecialByte.get(unescapedArray[i]);
098                                os.write(specialByte.escapeByte());
099                        } else
100                                os.write(unescapedArray[i]);
101                }
102                return os.toByteArray();
103        }
104
105        /**
106         * Returns the packet data.
107         * 
108         * @return The packet data.
109         */
110        public abstract byte[] getPacketData();
111
112        /**
113         * Returns the packet length.
114         * 
115         * @return The packet length.
116         */
117        public int getPacketLength() {
118                byte[] packetData = getPacketData();
119                if (packetData == null)
120                        return 0;
121                return packetData.length;
122        }
123        
124        /**
125         * Returns the packet checksum.
126         * 
127         * <p>To calculate: Not including frame delimiters and length, add all 
128         * bytes keeping only the lowest 8 bits of the result and subtract the 
129         * result from {@code 0xFF}.</p>
130         * 
131         * @return The packet checksum.
132         */
133        public int getChecksum() {
134                checksum.reset();
135                byte[] packetData = getPacketData();
136                if (packetData != null)
137                        checksum.add(packetData);
138                return (byte)checksum.generate() & 0xFF;
139        }
140        
141        /**
142         * Returns a map with the XBee packet parameters and their values.
143         * 
144         * @return A sorted map containing the XBee packet parameters with their 
145         *         values.
146         */
147        public LinkedHashMap<String, String> getParameters() {
148                LinkedHashMap<String, String> parameters = new LinkedHashMap<String, String>();
149                parameters.put("Start delimiter", HexUtils.integerToHexString(SpecialByte.HEADER_BYTE.getValue(), 1));
150                parameters.put("Length", HexUtils.prettyHexString(HexUtils.integerToHexString(getPacketLength(), 2)) + " (" + getPacketLength() + ")");
151                parameters.putAll(getPacketParameters());
152                parameters.put("Checksum", toString().substring(toString().length() - 2));
153                return parameters;
154        }
155        
156        /**
157         * Returns a map with the XBee packet parameters and their values.
158         * 
159         * @return A sorted map containing the XBee packet parameters with their 
160         *         values.
161         */
162        protected abstract LinkedHashMap<String, String> getPacketParameters();
163        
164        /*
165         * (non-Javadoc)
166         * @see java.lang.Object#toString()
167         */
168        @Override
169        public String toString() {
170                return HexUtils.byteArrayToHexString(generateByteArray());
171        }
172        
173        /**
174         * Returns a pretty string representing the packet.
175         * 
176         * @return Pretty String representing the packet.
177         */
178        public String toPrettyString() {
179                String value = "Packet: " + toString() + "\n";
180                LinkedHashMap<String, String> parameters = getParameters();
181                for (String parameter:parameters.keySet())
182                        value = value + parameter + ": " + parameters.get(parameter) + "\n";
183                return value;
184        }
185        
186        /**
187         * Parses the given hexadecimal string and returns a Generic XBee packet. 
188         * 
189         * <p>The string can contain white spaces.</p>
190         * 
191         * @param packet The hexadecimal string to parse.
192         * @param mode The operating mode to parse the packet (API 1 or API 2).
193         * 
194         * @return The generated Generic XBee Packet.
195         * 
196         * @throws IllegalArgumentException if {@code mode != OperatingMode.API } and
197         *                                  if {@code mode != OperatingMode.API_ESCAPE}.
198         * @throws InvalidPacketException if the given string does not represent a 
199         *                                valid frame: invalid checksum, length, 
200         *                                start delimiter, etc.
201         * @throws NullPointerException if {@code packet == null}.
202         * 
203         * @see com.digi.xbee.api.models.OperatingMode#API
204         * @see com.digi.xbee.api.models.OperatingMode#API_ESCAPE
205         */
206        public static XBeePacket parsePacket(String packet, OperatingMode mode) throws InvalidPacketException {
207                if (packet == null)
208                        throw new NullPointerException("Packet cannot be null.");
209                        
210                return parsePacket(HexUtils.hexStringToByteArray(packet.trim().replace(" ",  "")), mode);
211        }
212        
213        /**
214         * Parses the given byte array and returns a Generic XBee packet.
215         * 
216         * @param packet The byte array to parse.
217         * @param mode The operating mode to parse the packet (API 1 or API 2).
218         * 
219         * @return The generated Generic XBee Packet.
220         * 
221         * @throws IllegalArgumentException if {@code mode != OperatingMode.API } and
222         *                                  if {@code mode != OperatingMode.API_ESCAPE} 
223         *                                  or if {@code packet.length == 0}.
224         * @throws InvalidPacketException if the given byte array does not represent 
225         *                                a valid frame: invalid checksum, length, 
226         *                                start delimiter, etc.
227         * @throws NullPointerException if {@code packet == null}.
228         * 
229         * @see com.digi.xbee.api.models.OperatingMode#API
230         * @see com.digi.xbee.api.models.OperatingMode#API_ESCAPE
231         */
232        public static XBeePacket parsePacket(byte[] packet, OperatingMode mode) throws InvalidPacketException {
233                if (packet == null)
234                        throw new NullPointerException("Packet byte array cannot be null.");
235                
236                if (mode != OperatingMode.API && mode != OperatingMode.API_ESCAPE)
237                        throw new IllegalArgumentException("Operating mode must be API or API Escaped.");
238                
239                if (packet.length == 0)
240                        throw new IllegalArgumentException("Packet length should be greater than 0.");
241                
242                if (packet.length > 1 && ((packet[0] & 0xFF) != SpecialByte.HEADER_BYTE.getValue()))
243                        throw new InvalidPacketException("Invalid start delimiter.");
244                
245                XBeePacketParser parser = new XBeePacketParser();
246                XBeePacket xbeePacket = parser.parsePacket(new ByteArrayInputStream(packet, 1, packet.length - 1), mode);
247                return xbeePacket;
248        }
249}