In Data To The House we talked about XBee sensor meshes and configured a couple of XBee radios to get data from the greenhouse into the house. In this article, we will
- wire an XBee Series 2 radio to the greenhouse sensor board
- modify the Teensy sketch to send the data from the sensors to the XBee on the sensor board
- write a small Python script to read the sensor data from the Coordinator radio
The Greenhouse Sensor Board
The Greenhouse sensor board needs to have the XBee radio added and the Teensy sketch modified to send data through the sensor board radio to the Coordinator radio.
Wiring in the XBee Radio
Wiring the XBee radio to the sensor board is very easy. The XBee radio uses standard serial communication to talk to a host processor. The RX and TX pins on the radio will be attached to TX1 and RX1 respectively on the Teensy. RX1 and TX1 are the first hardware-based serial pins on the Teensy 3.1. The serial connection and power are all that are needed. The XBee is in the lower right of the image below.
The current version of the board then looks like this.
This is a more up close shot of the board.
Originally I thought I was going to have to use an XBee Series 2 Pro radio to get data all the way across the yard from the greenhouse to the house, going through outer walls of both buildings and th Coordinator possibly in the basement of the house. The Pro is 63 mW of power and has a distance of about 1 mile, while the standard radios are 2 mW and good for about 400 feet. A quick test with a 2 mW radio got signals from inside the closed greenhouse into the basement of the main house, so I stayed with the 2 mW radio. This was a relief as the 2 mW radio uses up only 40 mA of power, while the Pro uses 295 mA and I would have had to build an external power board as the power regulator on the Teensy couldn't have powered the Pro.
Modifying the Teensy Sketch
The code running on the Teensy must now be modified to use the radio. The code can be found in the github repository here.
First make sure you have Andrew Rapp's XBee library for Arduino installed on your machine. At some point I will probably write a new Arduino XBee library as Andrew's code is GPL. Though I fully support people using the Gnu license, I much prefer the Apache 2 license and so will most likely write a library that will be licensed as such.
First you will need to include the XBee.h file in your sketch.
#include <XBee.h>
The sensor data will be sent as an XBee TX packet. TX packets allow arbitrary data to be sent between radios and are received as an RX frame on the destination radio. Adding the following lines will create an XBee radio object and some other data structures for the XBee TX packet.
XBee xbee; XBeeAddress64 addr64 = XBeeAddress64(0x00000000, 0x00000000); ZBTxRequest zbTx = ZBTxRequest(addr64, (uint8_t *)&sensorData, sizeof(sensorData));
The variable addr64 contains the 64 bit address of the radio that will receive the sensor data packets. Typically I use the Coordinator radio to be the main destination for sensor data, so here set the destination address to the special address 0000000000000000 for the Coordinator. You could also use the exact address for the Coordinator by looking at the underside of the radio, but by using the special address you can switch out your Coordinator radio for another radio and the Teensy code would not have to change.
The variable zbTx creates the TX request packet to be sent by the XBee. It uses our address for the Coordinator Radio. The last argument sizeof(sensorData) will give the number of bytes of sensor data to be transmitted. This way we can add or subtract data from the packet and not have to worry about counting bytes on the Teensy side.
The second argument (uint8_t *)&sensorData needs some explanation. The ZBTxRequest constructor requires a pointer to an array of bytes containing the data to be transmitted to the destination radio, an expression of type uint_8 *. sensorData contains our data, but it is a bunch of float data. How to we get those float values into an array of bytes? A lot of people create a byte array for the TX packet and use various tricks to get the bytes from the floats into the byte array, but that is way too much work. Can we do better?
Let's remind ourselves of the sensor data data structure.
When sensorData is laid out in the memory of the Teensy, the first 4 bytes will contain the value of temperatureInside, the next 4 bytes will contain the value for humidityInside, the next 4 temperatureOutside, and so on. Whatever order the fields are found in the struct will be the order of the values in the Teensy memory.
Though it will not matter in this article, the float values are laid out in memory lowest order byte first. This is called little-endian. Knowing the byte order will matter when we start processing the data in Java, but won't matter for the Python script.
Going back to our second argument, the expression &sensorData will give us the memory address of the lowest byte of the sensor data. Just to reinforce, this byte will be the low order byte (remember little-endian) of temperatureInside. However, the type of this pointer will be SensorData * and we need a pointer type of uint_8 *, so we use the type coercion (uint_8 *).
The program then places data in the structure as before. It doesn't matter what order we make the assignments in, the only thing that matters as far as the radios are concerned is the order of the fields in the struct.
Initializing the xbee object is pretty easy. First, the serial object needs to be initialized. Since the TX1/RX1 pins are being used on the Teensy for the XBee, we need to use the Serial1 object.
The final new line sends the actual packet to the destination radio.
Let's look at the output coming across the USB serial connection for the Teensy. Once the Python script is written, we will want to confirm that the values being output here are the same being output by the Python script.
The variable zbTx creates the TX request packet to be sent by the XBee. It uses our address for the Coordinator Radio. The last argument sizeof(sensorData) will give the number of bytes of sensor data to be transmitted. This way we can add or subtract data from the packet and not have to worry about counting bytes on the Teensy side.
The second argument (uint8_t *)&sensorData needs some explanation. The ZBTxRequest constructor requires a pointer to an array of bytes containing the data to be transmitted to the destination radio, an expression of type uint_8 *. sensorData contains our data, but it is a bunch of float data. How to we get those float values into an array of bytes? A lot of people create a byte array for the TX packet and use various tricks to get the bytes from the floats into the byte array, but that is way too much work. Can we do better?
Let's remind ourselves of the sensor data data structure.
typedef struct _SensorData { float temperatureInside; float humidityInside; float temperatureOutside; float humidityOutside; float altitude; float barometricPressure; } SensorData; SensorData sensorData;
When sensorData is laid out in the memory of the Teensy, the first 4 bytes will contain the value of temperatureInside, the next 4 bytes will contain the value for humidityInside, the next 4 temperatureOutside, and so on. Whatever order the fields are found in the struct will be the order of the values in the Teensy memory.
Though it will not matter in this article, the float values are laid out in memory lowest order byte first. This is called little-endian. Knowing the byte order will matter when we start processing the data in Java, but won't matter for the Python script.
Going back to our second argument, the expression &sensorData will give us the memory address of the lowest byte of the sensor data. Just to reinforce, this byte will be the low order byte (remember little-endian) of temperatureInside. However, the type of this pointer will be SensorData * and we need a pointer type of uint_8 *, so we use the type coercion (uint_8 *).
The program then places data in the structure as before. It doesn't matter what order we make the assignments in, the only thing that matters as far as the radios are concerned is the order of the fields in the struct.
Initializing the xbee object is pretty easy. First, the serial object needs to be initialized. Since the TX1/RX1 pins are being used on the Teensy for the XBee, we need to use the Serial1 object.
Serial1.begin(9600); xbee = XBee(); xbee.begin(Serial1);
The final new line sends the actual packet to the destination radio.
xbee.send(zbTx);
Let's look at the output coming across the USB serial connection for the Teensy. Once the Python script is written, we will want to confirm that the values being output here are the same being output by the Python script.
So far so good!
Reading the sensors after 1 second pauses is probably too frequent. When the board is finally deployed, I will probably sample every 10 minutes, which means that the SLEEP_DELAY constant at the beginning of the file should be set to 60000. But waiting 10 minutes while debugging the system is painful, so for now the value is 1000 for those 1 second pauses.
Reading the sensors after 1 second pauses is probably too frequent. When the board is finally deployed, I will probably sample every 10 minutes, which means that the SLEEP_DELAY constant at the beginning of the file should be set to 60000. But waiting 10 minutes while debugging the system is painful, so for now the value is 1000 for those 1 second pauses.
Checking Radio Communication
It is always best to test in as simple an environment as you can. Now that the sensor board has its radio, let's use XCTU to see if the board is communicating with the Coordinator radio before writing the Python script to read the data.
If you remember the last article, we talked some about Networking mode in XCTU. This mode lets us look at all of the radios in the mesh and see who can talk to whom. Let's use that now.
First I placed the greenhouse sensor board in the greenhouse and plugged it into power. Then I went back into the house and plugged the Coordinator radio into a Sparkfun USB Explorer and plugged the USB cable into my laptop and set up XCTU to look at the radio. A quick check in Networking mode (remember to hit the Scan button) showed the radios talking to each other.
I then placed another XBee Router next to the window in the main house and carried my laptop into the basement, expecting to see the greenhouse radio transmitting through the Router radio by the window which would then communicate the sensor packets to the Coordinator radio. But if you look at the picture below, you can see that the greenhouse radio is talking directly to the Coordinator radio despite the distance from the house to the greenhouse, 2 outer walls, and the distance into the basement.
Not bad!
If you remember in the last article we talked about XCTU's Consoles mode which allows us to look at the packets coming into a radio. I switched over to the Consoles window and clicked the Open button. You can see the XBee API RX packets coming into the radio from the greenhouse radio. They are the red lines in the center section of the picture below. They are labeled Receive Packet and have a length of 36 bytes.
There is a window along the right side of the Consoles window that shows the actual contents of the packet. If you scroll this window to the bottom and hit the Hex tab, you can see the sensor data coming through.
Receiving the Data from the Coordinator Radio
Now that we know data is coming into the Coordinator radio from the sensor radio, it is time to write a Python script to show us the data. The complete script can be found here on github.
The first thing to do is install the Python XBee library on the computer you want to run the script on.
$ sudo pip install xbee
First we need the Python object that interfaces to a Series 2 radio in API mode. This is done by importing the ZigBee package from the xbee Python library.
Next we will create a serial connection to the Sparkfun USB Explorer and hand this serial connection to the ZigBee object.
The ZigBee object can now be polled to see if a new API frame has been read. The wait_read_frame() call blocks until a new frame is read.
Once a frame is received, we will first check to see if it is from the greenhouse radio. The reason for this check is that eventually all radios will be sending TX packets to the Coordinator and we need some way to tell which radio has sent a particular packet. This can be handled in several ways. We could preface each packet with an identifier saying what set of sensors the data is from. Or we could just know the address of each radio, assuming each radio only sends one type of data packet. I chose in this case to go with the latter.
The frame that the ZigBee class creates is a dictionary with multiple fields in it, including the address of the source radio of the TX packet, as well as the data. To get the 8 bytes of source address, we use the expression frame['source_addr_long']. We take these 8 bytes and create a hexadecimal string for easy comparison with the address supplied as a command line argument when we start running the script.
If we find that we have a packet from the greenhouse radio, we then need to decode the binary data in the packet to the series of floats that were sent. We use the expression frame['rf_data'] to get this data.
To read the data we need to know what order the floats are in the packet. Looking back at the struct in the Teensy code we know the order is
We can read these values from the TX data by specifying the start location and the number of bytes to use for a data value. In our case, every value being sent is a float and a float is 4 bytes.
Notice the inside temperature is first in the packet with its first byte at position 0 in rf_data, so we look at rf_data[0:4]. The inside humidity is second in the packet with its first byte at position 4, so we need to look at rf_data[4:8]. The rest of the values follows the same pattern.
The script can be run with something like the following command. For me the Sparkfun USB Explorer had ended up at /dev/ttyUSB0 and the greenhouse sensor radio has the 64 bit address 0013a200407bd2e6.
The screenshot below shows the data being output by the script.
from xbee import ZigBee
Next we will create a serial connection to the Sparkfun USB Explorer and hand this serial connection to the ZigBee object.
serial_port = serial.Serial(serial_port_name, SERIAL_BAUD_RATE) xbee = ZigBee(serial_port, escaped=True)
The ZigBee object can now be polled to see if a new API frame has been read. The wait_read_frame() call blocks until a new frame is read.
Once a frame is received, we will first check to see if it is from the greenhouse radio. The reason for this check is that eventually all radios will be sending TX packets to the Coordinator and we need some way to tell which radio has sent a particular packet. This can be handled in several ways. We could preface each packet with an identifier saying what set of sensors the data is from. Or we could just know the address of each radio, assuming each radio only sends one type of data packet. I chose in this case to go with the latter.
The frame that the ZigBee class creates is a dictionary with multiple fields in it, including the address of the source radio of the TX packet, as well as the data. To get the 8 bytes of source address, we use the expression frame['source_addr_long']. We take these 8 bytes and create a hexadecimal string for easy comparison with the address supplied as a command line argument when we start running the script.
If we find that we have a packet from the greenhouse radio, we then need to decode the binary data in the packet to the series of floats that were sent. We use the expression frame['rf_data'] to get this data.
To read the data we need to know what order the floats are in the packet. Looking back at the struct in the Teensy code we know the order is
- The inside temperature
- The inside humidity
- The outside temperature
- The outside humidity
- The altitude
- The barometric pressure
We can read these values from the TX data by specifying the start location and the number of bytes to use for a data value. In our case, every value being sent is a float and a float is 4 bytes.
inside_temperature = struct.unpack('f',rf_data[0:4])[0] inside_humidity = struct.unpack('f',rf_data[4:8])[0] outside_temperature = struct.unpack('f',rf_data[8:12])[0] outside_humidity = struct.unpack('f',rf_data[12:16])[0] altitude = struct.unpack('f',rf_data[16:20])[0] barometric_pressure = struct.unpack('f',rf_data[20:24])[0]
Notice the inside temperature is first in the packet with its first byte at position 0 in rf_data, so we look at rf_data[0:4]. The inside humidity is second in the packet with its first byte at position 4, so we need to look at rf_data[4:8]. The rest of the values follows the same pattern.
The script can be run with something like the following command. For me the Sparkfun USB Explorer had ended up at /dev/ttyUSB0 and the greenhouse sensor radio has the 64 bit address 0013a200407bd2e6.
$ ./GreenhouseSensors.py /dev/ttyUSB0 0013a200407bd2e6
The screenshot below shows the data being output by the script.
Conclusion
At long last there is finally data making it from the greenhouse into the main house. You have seen how easy it is to create and process XBee packets both on the Teensy and in a Python script. Ultimately I will write an Interactive Spaces activity to process the data, but for now the Python script is enough. I will also soon modify the Python script to send the data up into the time series database in the cloud as discussed in the article Sensor Data To The Cloud: Part 1.
No comments:
Post a Comment
Note: Only a member of this blog may post a comment.