I wanted to have a better understanding of home automation -- something beyond, buy this product and connect it to this hub to magically control your lights from your phone. I wanted to know what happens under all the layers of graphical interface and cloud-based services.
The Arduino Nano 33 Sense and a Raspberry Pi make a perfect pair to explore the basics of Bluetooth Low Energy (BLE) devices and how to gather data from them. All the required hardware is built-in to both the Arduino and the Pi, making it easy to get started.
The software consists of an Arduino sketch and some basic Python programs.
This is a simple tutorial, and not very useful as an end product. The aim is more in learning the basics and using the knowledge as the building blocks for bigger and better things.
The Arduino SketchI've linked to a sketch I created called BluetoothWeather.ino. As Bluetooth goes, it's fairly simple. But, it's definitely a bit more involved than a simple sketch like flashing an LED.
For now, I'll assume you're content to copy and paste and I'll go into the details later.
Once the sketch is flashed to the Nano 33 BLE Sense, it will wait for Bluetooth Low Energy connections and report temperature, humidity, and barometric pressure readings to any device that connects.
Nordic Semiconductor's nRF Connect app can be used to verify the sketch is working. Simply scan for devices and connect to the one named Nano33BLE.
The Raspberry Pi SetupThe Raspberry Pi 3 was the first to feature an integrated Bluetooth adapter. In this tutorial, I'm using a Raspberry Pi 3 B+ with the Raspbian OS Lite loaded. (Lite is the version that has a command-line interface and no GUI.)
There are plenty of resources giving instructions on how to download and flash an SD Card for Raspberry Pi OS, so that won't be covered here. However, I will suggest looking up how to activate SSH if you plan to run the Pi without keyboard and monitor. Tom's Hardware has a nice article covering the procedure.
Other than that, everything else is part of the base OS. So, let's boot up the Raspberry Pi and get started.
bluetoothctlRaspberry Pi OS is based on the Debian Linux distribution. Linux comes with a Bluetooth command-line tool called bluetoothctl. This is what we'll be using to make a test connection to the Arduino Nano and gather data from the sensors. If everything checks out, we can move on to a more advanced example using Python.
Simply run sudo bluetoothctl
at the command prompt.
Scanning for devices
Before we can connect to the Arduino, we have to find it. This is done with the command scan on
.
Once the device is found, we'll use scan off
to tell bluetoothctl to stop looking for devices.
Here's an example of starting up bluetoothctl and finding the Nano.
$ sudo bluetoothctl
[bluetooth]# scan on
[CHG] Device FB:A7:C6:D4:64:C4 Name: Nano33BLE
[CHG] Device FB:A7:C6:D4:64:C4 Alias: Nano33BLE
[bluetooth]# scan off
Notice the six pairs of hex digits displayed along with the name Nano33BLE. This is the Arduino's Bluetooth hardware address. It is unique and never changes. The name Nano33BLE can be changed in the Arduino sketch, but the hardware address remains the same.
Copy the hardware address displayed by your Raspberry Pi. Keep is somewhere handy, because it's going to be used again. In fact, once you know the address, scanning is no longer necessary.
Connecting to the NanoBluetooth Low Energy devices behave differently than traditional Bluetooth devices like keyboards and headsets. There's no pairing prerequisite when connecting to a BLE device, you simply connect.
Using bluetoothctl, the command is connect <address>
, where address is the string of six colon-separated hex digits copied from the scan output.
[bluetooth]# connect FB:A7:C6:D4:64:C4
[CHG] Device FB:A7:C6:D4:64:C4 Connected: yes
Connection successful
[NEW] Primary Service
/org/bluez/hci0/dev_FB_A7_C6_D4_64_C4/service0006
00001801-0000-1000-8000-00805f9b34fb
Generic Attribute Profile
...
[NEW] Primary Service
/org/bluez/hci0/dev_FB_A7_C6_D4_64_C4/service000a
0000181a-0000-1000-8000-00805f9b34fb
Environmental Sensing
...
[NEW] Characteristic
/org/bluez/hci0/dev_FB_A7_C6_D4_64_C4/service000a/char000f
00002a6e-0000-1000-8000-00805f9b34fb
Temperature
...
Upon connection, the advertised services are communicated. The output shown above has been truncated for clarity. Notice how the Temperature characteristic is shown along with the Environmental Sensing service. (Pressure and humidity are omitted in the example above, but will be shown when running the command.)
The lines in the Arduino sketch responsible for the output shown on the Raspberry Pi are:
BLEService environmentalSensingService("181A");
BLEShortCharacteristic temperatureCharacteristic("2A6E", BLERead);
BLE.setAdvertisedService(environmentalSensingService);
environmentalSensingService.addCharacteristic(temperatureCharacteristic);
BLE.addService(environmentalSensingService);
BLE.setConnectable(true);
BLE.advertise();
Reading CharacteristicsNow that the Raspberry Pi has a list of services and characteristics, it's relatively simple to read them using bluetoothctl's gatt sub-menu. (GATT is Bluetooth terminology for Generic ATTribute.)
Enter the GATT sub-menu with the command menu gatt
.
This gives a new set of commands that can be used. Two of the more interesting ones are list-attributes and select-attribute.
list-attributes is used to show the information about services and characteristics that we saw when the initial connection was made to the Nano. As long as we have that list, this command is not so important.
select-attribute allows us to focus on one particular characteristic offered by the Nano. In the example below, the commands are used to select the temperature characteristic from the Environmental Sensing service.
[Arduino]# menu gatt
select-attribute 00002a6e-0000-1000-8000-00805f9b34fb
[Arduino:/service000a/char000f]#
Notice how the characteristic is specified using it's Universally Unique ID (UUID.) This is similar to the way we refer to the Arduino by its hexadecimal hardware address rather than its name. The UUID should always work and never change.
Now that we've selected the UUID for the temperature characteristic, it's just a matter of using the read
command to get the temperature value.
[Arduino:/service000a/char000f]# read
Attempting to read /org/bluez/hci0/dev_FB_A7_C6_D4_64_C4/service000a/char000f
[CHG] Attribute /org/bluez/hci0/dev_FB_A7_C6_D4_64_C4/service000a/char000f Value:
4c 08 L.
4c 08 L.
Interpreting ValuesThe value returned from the read
command doesn't look much like the temperature reading you would get if you fetched the same characteristic value using nRF Connect on a mobile device. It doesn't look much like a temperature at all.
It's in there. It just takes a little work to make it readable for humans.
The Bluetooth GATT definition for temperature explains how to interpret the return value. The key is that it is a sixteen-bit signed integer, representing a Celsius temperature with two decimal places of resolution.
The sixteen-bit signed integer is easy. It's the 4c 08 displayed with least significant value first. This is not uncommon in low-level computer representations of numbers. Just rearrange the pairs of hex digits and the actual number is 0x084C.
Convert hex to decimal and we get 2124.
That seems a little hot, but we're not done yet. The other important piece of the GATT definition for temperature is that it has a resolution of two decimal places. How can an integer have two decimal places? It doesn't really, the decimal point is simply implied to exist so that there are always two decimal places.
Taking the number 2124 and inserting the decimal to give two decimal places results in 21.24. This is a much more reasonable Celsius temperature for a late September day.
To convert the value in a computer program, we would divide by 100. Other values, like barometric pressure have a resolution of one decimal place. For that, we only need to divide by 10. Examples of this can be seen in the Arduino sketch where floating point values from the sensors are converted to fixed-point integers for Bluetooth.
// Update Bluetooth characteristics with new values.
pressureCharacteristic.writeValue((uint32_t) round(p * 10000));
temperatureCharacteristic.writeValue((int16_t) round(t * 100));
humidityCharacteristic.writeValue((uint16_t) round (h * 100));
Notice how the floating-point values t and h, are multiplied by one-hundred to preserve the two decimal places of resolution in an integer format. Pressure, p, has a resolution of one decimal place and we would expect to see it multiplied by ten. But, the GATT document also states that pressure is in Pascals. The Arduino gives us kiloPascals. Therefore, we multiply by 1000 to convert to Pascals and by 10 to give the one decimal place of resolution. (10 x 1000 is 10000.)
Python and BleakObviously, bluetoothctl is not the ideal way to get information from the Arduino sensors. But, it's a good starting point to gain an understanding of how Bluetooth Low Energy works. The next step is making it more user friendly.
Many programming languages have Bluetooth libraries available. We've proved that the Raspberry Pi can successfully connect to, and gather environmental characteristic from, the Nano. In this section, we'll be using the Python programming language and a library called Bleak to gather GATT characteristics from the Arduino Nano.
Raspbian OS Lite does not include everything we need, so there are a few packages to fetch with the apt tool.
sudo apt install python3-pip
sudo pip3 install bleak
Once PIP3 and Bleak are installed, we're ready to go.
The Bleak library includes sample code that gives a good starting point for reading values from the Nano. The first sample on their home page shows how to discover devices. It's a lot like the bluetoothctl scan on
command.
Just like bluetoothctl, the Python program needs to be run as root in order to access the Raspberry Pi's Bluetooth device. After a moment, it will output a list of devices, including the Arduino Nano 33 BLE Sense. On my system, it looks like this:
$ sudo python3 ./discover.py
FB:A7:C6:D4:64:C4: Nano33BLE
The address can be copied and used in the next example on the Bleak Library home page. Just replace the line that reads: address = "24:71:89:cc:09:05"
with the address produced by the discover script.
Running the model number reading script copied directly from the Bleak home page will fail. That's because it's looking for a characteristic (2A24) that is not defined in the Arduino sketch. We need to make a change in the Python code so MODEL_NBR_UUID = "00002a24-0000-1000-8000-00805f9b34fb"
is MODEL_NBR_UUID = "00002a00-0000-1000-8000-00805f9b34fb"
It's not a big change. Only two digits in the first set of UUID numbers are changed: 2a24 becomes 2a00. Everything after that is left as-is. Once this is done, the Python script runs without error.
$ sudo python3 ./read_model.py
Model Number: Arduino
This small change of the UUID is all it takes to read any GATT characteristic we want. The only additional work is to make sure the print() function shows the correct variable type.
To read humidity from the Nano, only two lines need to be changed, the UUID and the print() statement. The UUID becomes:
00002a6f-0000-1000-8000-00805f9b34fb
And the print() statement becomes:
print("Humidity: {0}%".format(int.from_bytes(humidity, byteorder='little', signed=False) / 100))
These minor changes to the Bleak sample code is all it takes to read any GATT characteristic. Python programs for reading all the environmental sensing characteristics from the Arduino Nano are included in the Code section.
Next StepsWith the Bleak library and a little Python scripting knowledge the Raspberry Pi could be set up to take periodic readings and display them on a monitor or web page. It could even go as far as analyzing trends to make predictions about coming weather.
The Arduino sketch could be improved to forcibly disconnect clients after a certain time period. Bluetooth Low Energy is meant to be a connect-read/write-disconnect kind of operation. Any client that hangs on too long is blocking others from reading.
By this point, hopefully you have the confidence and skills to take on one or both of these tasks to build your ultimate Bluetooth Low Energy weather station.
Appendix: Sketch DetailsAs promised, here is some further insight into the Bluetooth-specific parts of the Arduino sketch. I have divided it into variables, setup and loop.
Variables
Declaring service and characteristic objects is done before anything else. This makes them global.
BLEService environmentalSensingService("181A");
BLEUnsignedIntCharacteristic pressureCharacteristic("2A6D", BLERead);
BLEShortCharacteristic temperatureCharacteristic("2A6E", BLERead);
BLEUnsignedIntCharacteristic humidityCharacteristic("2A6F", BLERead);
The four-digit hex values inside the quotes are GATT UUIDs used by the object constructor. These shorter 16-bit UUIDs are predefined in the Bluetooth Low Energy GATT specification. You cannot use them for anything other than their predefined purpose.
181A is the Environmental Sensing service. The other UUIDs refer to Characteristics. I did not find any rule that says certain characteristics can only go with certain services, but most of them logically match up. Temperature, humidity, and pressure are a good fit for a weather related Environmental Sensing service.
But, there is also a Battery service. Could a Battery service use a temperature? Many battery charging circuits do monitor temperature to detect unsafe conditions, so it seems logical. I just haven't tried it.
As for the BLERead
, this specifies the characteristics can be read, but not written. It makes sense for weather data. You don't set the barometric pressure, you read it. There are also writable values and a flag for notify. Notify is a way to push updated values to known clients. It's used to cut down on client polling.
The other critical aspect of declaring these objects involves getting the proper variable type.
Notice that temperature is a short integer while humidity is an unsigned integer. This makes sense, because Celsius temperature can range from positive to negative, but relative humidity is always between 0 and 100 percent.
The variable typing has to be exactly what is specified in the Bluetooth GATT definitions. You cannot make temperature a floating point characteristic. (Technically, you can, but the value will be unusable.) You have to follow the GATT definitions for the characteristics.
Setup
After stripping out code for debugging and driving the status LED, Bluetooth setup consists of:
Setting up a name and service
BLE.setLocalName("Nano33BLE");
BLE.setAdvertisedService(environmentalSensingService);
The name Nano33BLE can be whatever you want it to be. If you had two, one inside and one outdoors, they could be labeled accordingly.
Adding characteristics to the service
environmentalSensingService.addCharacteristic(pressureCharacteristic);
environmentalSensingService.addCharacteristic(temperatureCharacteristic);
environmentalSensingService.addCharacteristic(humidityCharacteristic);
These three lines of code associate the characteristics with the service. If you use the nRF connect mobile app, you will see these displayed hierarchically underneath the associated service.
The last bit involves ensuring clients can connect and advertising the service we have.
BLE.setConnectable(true);
BLE.advertise();
Don't forget these or you'll be wondering why it doesn't work.
Loop
In the main loop, we have:
if (central) {
while (central.connected()) {
pressureCharacteristic.writeValue((uint32_t) round(p * 10000));
temperatureCharacteristic.writeValue((int16_t) round(t * 100));
humidityCharacteristic.writeValue((uint16_t) round(h * 100));
delay(1000);
}
}
Central is another name for the client device making a connection to the Nano. The central could be the Raspberry Pi or nRF Connect.
The logic here says that when there is a client, keep looping and updating values for the characteristics as long as that client remains connected. This lets the client get fresh data without having to disconnect and reconnect. The delay is there to keep the values a little more steady. (They can't change more than once per second.)
The only trouble with this loop is that a client could hold onto the connection indefinitely, either intentionally or by simply forgetting to disconnect. This ties up the connection prevents other clients from reading any of the service characteristics. (Note: This is a limitation of the software sketch. The nRF52840 chip in the Nano is capable of four simultaneous client connections.)
It would be a nice addition to have a counter keeping track of how many iterations a client is connected. After a threshold value, a disconnect could be forced.
Comments