001/**
002 * Copyright 2017, Digi International Inc.
003 *
004 * This Source Code Form is subject to the terms of the Mozilla Public
005 * License, v. 2.0. If a copy of the MPL was not distributed with this
006 * file, you can obtain one at http://mozilla.org/MPL/2.0/.
007 *
008 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 
009 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 
010 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 
011 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 
012 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 
013 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 
014 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
015 */
016package com.digi.xbee.api.packet;
017
018import java.io.ByteArrayInputStream;
019import java.io.ByteArrayOutputStream;
020import java.util.Arrays;
021import java.util.LinkedHashMap;
022
023import com.digi.xbee.api.exceptions.InvalidPacketException;
024import com.digi.xbee.api.models.SpecialByte;
025import com.digi.xbee.api.models.OperatingMode;
026import com.digi.xbee.api.utils.ByteUtils;
027import com.digi.xbee.api.utils.HexUtils;
028
029/**
030 * This abstract class represents the basic structure of an XBee packet.
031 * 
032 * <p>Derived classes should implement their own payload generation depending 
033 * on their type.</p>
034 * 
035 * <p>Generic actions like checksum compute or packet length calculation is 
036 * performed here.</p>
037 */
038public abstract class XBeePacket {
039
040        private static final int HASH_SEED = 23;
041        
042        // Variables.
043        private XBeeChecksum checksum;
044        
045        /**
046         * Class constructor. Instantiates a new {@code XBeePacket} object.
047         */
048        protected XBeePacket() {
049                checksum = new XBeeChecksum();
050        }
051
052        /**
053         * Generates the XBee packet byte array. 
054         * 
055         * <p>Use only while working in API mode 1. If API mode is 2, use 
056         * {@link #generateByteArrayEscaped()}.</p>
057         * 
058         * @return The XBee packet byte array.
059         * 
060         * @see #generateByteArrayEscaped()
061         */
062        public byte[] generateByteArray() {
063                checksum.reset();
064                byte[] packetData = getPacketData();
065                ByteArrayOutputStream os = new ByteArrayOutputStream();
066                os.write(SpecialByte.HEADER_BYTE.getValue());
067                if (packetData != null) {
068                        byte[] length = ByteUtils.shortToByteArray((short)packetData.length);
069                        int msb = length[0];
070                        int lsb = length[1];
071                        os.write(msb);
072                        os.write(lsb);
073                        for (int i = 0; i < packetData.length; i++) {
074                                checksum.add(packetData[i]);
075                                os.write(packetData[i]);
076                        }
077                } else {
078                        os.write(0);
079                        os.write(0);
080                }
081                os.write((byte)checksum.generate() & 0xFF);
082                return os.toByteArray();
083        }
084
085        /**
086         * Generates the XBee packet byte array escaping the special bytes.
087         * 
088         * <p>Use only while working in API mode 2. If API mode is 1 use 
089         * {@link #generateByteArray()}.</p>
090         * 
091         * @return The XBee packet byte array with escaped characters.
092         * 
093         * @see #generateByteArray()
094         */
095        public byte[] generateByteArrayEscaped() {
096                byte[] unescapedArray = generateByteArray();
097                ByteArrayOutputStream os = new ByteArrayOutputStream();
098                // Write header byte and do not escape it.
099                os.write(SpecialByte.HEADER_BYTE.getValue());
100                for (int i = 1; i < unescapedArray.length; i++) {
101                        // Start at 1 to avoid escaping header byte.
102                        if (SpecialByte.isSpecialByte(unescapedArray[i])) {
103                                os.write(SpecialByte.ESCAPE_BYTE.getValue());
104                                SpecialByte specialByte = SpecialByte.get(unescapedArray[i]);
105                                os.write(specialByte.escapeByte());
106                        } else
107                                os.write(unescapedArray[i]);
108                }
109                return os.toByteArray();
110        }
111
112        /**
113         * Returns the packet data.
114         * 
115         * @return The packet data.
116         */
117        public abstract byte[] getPacketData();
118
119        /**
120         * Returns the packet length.
121         * 
122         * @return The packet length.
123         */
124        public int getPacketLength() {
125                byte[] packetData = getPacketData();
126                if (packetData == null)
127                        return 0;
128                return packetData.length;
129        }
130        
131        /**
132         * Returns the packet checksum.
133         * 
134         * <p>To calculate: Not including frame delimiters and length, add all 
135         * bytes keeping only the lowest 8 bits of the result and subtract the 
136         * result from {@code 0xFF}.</p>
137         * 
138         * @return The packet checksum.
139         */
140        public int getChecksum() {
141                checksum.reset();
142                byte[] packetData = getPacketData();
143                if (packetData != null)
144                        checksum.add(packetData);
145                return (byte)checksum.generate() & 0xFF;
146        }
147        
148        /**
149         * Returns a map with the XBee packet parameters and their values.
150         * 
151         * @return A sorted map containing the XBee packet parameters with their 
152         *         values.
153         */
154        public LinkedHashMap<String, String> getParameters() {
155                LinkedHashMap<String, String> parameters = new LinkedHashMap<String, String>();
156                parameters.put("Start delimiter", HexUtils.integerToHexString(SpecialByte.HEADER_BYTE.getValue(), 1));
157                parameters.put("Length", HexUtils.prettyHexString(HexUtils.integerToHexString(getPacketLength(), 2)) + " (" + getPacketLength() + ")");
158                parameters.putAll(getPacketParameters());
159                parameters.put("Checksum", toString().substring(toString().length() - 2));
160                return parameters;
161        }
162        
163        /**
164         * Returns a map with the XBee packet parameters and their values.
165         * 
166         * @return A sorted map containing the XBee packet parameters with their 
167         *         values.
168         */
169        protected abstract LinkedHashMap<String, String> getPacketParameters();
170        
171        /*
172         * (non-Javadoc)
173         * @see java.lang.Object#equals(java.lang.Object)
174         */
175        @Override
176        public boolean equals(Object obj) {
177                if (!(obj instanceof XBeePacket))
178                        return false;
179                XBeePacket packet = (XBeePacket)obj;
180                
181                return Arrays.equals(packet.generateByteArray(), generateByteArray());
182        }
183        
184        /*
185         * (non-Javadoc)
186         * @see java.lang.Object#hashCode()
187         */
188        @Override
189        public int hashCode() {
190                int hash = HASH_SEED;
191                
192                byte [] array = generateByteArray();
193                for (byte b: array)
194                        hash = 31 * (hash + b);
195                return hash;
196        }
197        
198        /*
199         * (non-Javadoc)
200         * @see java.lang.Object#toString()
201         */
202        @Override
203        public String toString() {
204                return HexUtils.byteArrayToHexString(generateByteArray());
205        }
206        
207        /**
208         * Returns a pretty string representing the packet.
209         * 
210         * @return Pretty String representing the packet.
211         */
212        public String toPrettyString() {
213                String value = "Packet: " + toString() + "\n";
214                LinkedHashMap<String, String> parameters = getParameters();
215                for (String parameter:parameters.keySet())
216                        value = value + parameter + ": " + parameters.get(parameter) + "\n";
217                return value;
218        }
219        
220        /**
221         * Parses the given hexadecimal string and returns a Generic XBee packet. 
222         * 
223         * <p>The string can contain white spaces.</p>
224         * 
225         * @param packet The hexadecimal string to parse.
226         * @param mode The operating mode to parse the packet (API 1 or API 2).
227         * 
228         * @return The generated Generic XBee Packet.
229         * 
230         * @throws IllegalArgumentException if {@code mode != OperatingMode.API } and
231         *                                  if {@code mode != OperatingMode.API_ESCAPE}.
232         * @throws InvalidPacketException if the given string does not represent a 
233         *                                valid frame: invalid checksum, length, 
234         *                                start delimiter, etc.
235         * @throws NullPointerException if {@code packet == null}.
236         * 
237         * @see com.digi.xbee.api.models.OperatingMode#API
238         * @see com.digi.xbee.api.models.OperatingMode#API_ESCAPE
239         */
240        public static XBeePacket parsePacket(String packet, OperatingMode mode) throws InvalidPacketException {
241                if (packet == null)
242                        throw new NullPointerException("Packet cannot be null.");
243                        
244                return parsePacket(HexUtils.hexStringToByteArray(packet.trim().replace(" ",  "")), mode);
245        }
246        
247        /**
248         * Parses the given byte array and returns a Generic XBee packet.
249         * 
250         * @param packet The byte array to parse.
251         * @param mode The operating mode to parse the packet (API 1 or API 2).
252         * 
253         * @return The generated Generic XBee Packet.
254         * 
255         * @throws IllegalArgumentException if {@code mode != OperatingMode.API } and
256         *                                  if {@code mode != OperatingMode.API_ESCAPE} 
257         *                                  or if {@code packet.length == 0}.
258         * @throws InvalidPacketException if the given byte array does not represent 
259         *                                a valid frame: invalid checksum, length, 
260         *                                start delimiter, etc.
261         * @throws NullPointerException if {@code packet == null}.
262         * 
263         * @see com.digi.xbee.api.models.OperatingMode#API
264         * @see com.digi.xbee.api.models.OperatingMode#API_ESCAPE
265         */
266        public static XBeePacket parsePacket(byte[] packet, OperatingMode mode) throws InvalidPacketException {
267                if (packet == null)
268                        throw new NullPointerException("Packet byte array cannot be null.");
269                
270                if (mode != OperatingMode.API && mode != OperatingMode.API_ESCAPE)
271                        throw new IllegalArgumentException("Operating mode must be API or API Escaped.");
272                
273                if (packet.length == 0)
274                        throw new IllegalArgumentException("Packet length should be greater than 0.");
275                
276                if (packet.length > 1 && ((packet[0] & 0xFF) != SpecialByte.HEADER_BYTE.getValue()))
277                        throw new InvalidPacketException("Invalid start delimiter.");
278                
279                XBeePacketParser parser = new XBeePacketParser();
280                XBeePacket xbeePacket = parser.parsePacket(new ByteArrayInputStream(packet, 1, packet.length - 1), mode);
281                return xbeePacket;
282        }
283}