Pico W now supports Bluetooth both with MicroPython and C. But what is this, and why should you care?
The chances are you’ve used Bluetooth before, be it a keyboard, headphones, or sensor of some sort. It sends data back and forth via wireless radio signals. It typically has a range of a few metres to maybe tens of metres.
When you first start a Bluetooth device, you usually want it to start broadcasting some data. Often, this is for pairing, but it can also send a small amount of data to any other device in range. This is done using the Generic Access Profile (GAP).
GAP defines two roles: central and peripheral. Central devices are typically phones or computers, and they receive data from peripherals which tend to be sensors. Pico W can be either a central or a peripheral device.
You can simply continue sending data using the GAP, however, it only allows one-way communication, and each payload can only contain 31 bytes of data. You can send data both ways, gain more security, and generally get more features by connecting with a Generic Attribute Profile (GATT). The GATT defines the services and characteristics. In BLE terminology, a characteristic is a piece of data, and a service is a collection of characteristics. To use services, a device has to have a GATT that defines which services and characteristics they offer. These GATTs are predefined – here’s a list of them.
If you want to create a Bluetooth peripheral, the first thing you need to do is decide what GATT you want to use.
One crucial part of this is that the receiving software has to be expecting the type of data that your device is sending. For example, if you have a Bluetooth UART app on your phone, that won’t be able to communicate with a Bluetooth temperature sensor.
Profiles, services, and characteristics are all identified using Universally Unique Identifiers (UUIDs). A full list of all the numbers assigned to different things in Bluetooth is documented here.
Now we know a little about what’s going on, let’s take a look at an example. Note, you’ll also need to save the ble_advertising.py program to your Pico (under the same name).
We’ll use the example pico_ble_temperature_sensor.py from the pico-micropython-examples GitHub Repository.
In order to use this, you’ll need to flash the latest version of MicroPython to your Pico and you will need to save both the ble_advertising.py and pico_ble_temperature_sensor.py programs to Pico.
Here’s the code from the pico_ble_temperature_sensor.py program, which we’ll dissect to find out what’s going on.
# This example demonstrates a simple temperature sensor peripheral.
#
# The sensor's local value is updated, and it
will notify
# any connected central every 10 seconds.
import bluetooth
import random
import struct
import time
import machine
import ubinascii
from ble_advertising import advertising_payload
from micropython import const
from machine import Pin
_IRQ_CENTRAL_CONNECT = const(1)
_IRQ_CENTRAL_DISCONNECT = const(2)
_IRQ_GATTS_INDICATE_DONE = const(20)
_FLAG_READ = const(0x0002)
_FLAG_NOTIFY = const(0x0010)
_FLAG_INDICATE = const(0x0020)
# org.bluetooth.service.environmental_sensing
_ENV_SENSE_UUID = bluetooth.UUID(0x181A)
# org.bluetooth.characteristic.temperature
_TEMP_CHAR = (
bluetooth.UUID(0x2A6E),
_FLAG_READ | _FLAG_NOTIFY | _FLAG_INDICATE,
)
_ENV_SENSE_SERVICE = (
_ENV_SENSE_UUID,
(_TEMP_CHAR,),
)
# org.bluetooth.characteristic.gap.appearance.xml
_ADV_APPEARANCE_GENERIC_THERMOMETER = const(768)
class BLETemperature:
def __init__(self, ble, name=""):
self._sensor_temp = machine.ADC(4)
self._ble = ble
self._ble.active(True)
self._ble.irq(self._irq)
((self._handle,),) = self._ble.gatts_
register_services((_ENV_SENSE_SERVICE,))
self._connections = set()
if len(name) == 0:
name = 'Pico %s' % ubinascii.
hexlify(self._ble.config('mac')[1],':').decode().
upper()
print('Sensor name %s' % name)
self._payload = advertising_payload(
name=name, services=[_ENV_SENSE_UUID]
)
self._advertise()
def _irq(self, event, data):
# Track connections so we can send
notifications.
if event == _IRQ_CENTRAL_CONNECT:
conn_handle, _, _ = data
self._connections.add(conn_handle)
elif event == _IRQ_CENTRAL_DISCONNECT:
conn_handle, _, _ = data
self._connections.remove(conn_handle)
# Start advertising again to allow a
new connection.
self._advertise()
elif event == _IRQ_GATTS_INDICATE_DONE:
conn_handle, value_handle, status =
data
def update_temperature(self, notify=False,
indicate=False):
# Write the local value, ready for a
central to read.
temp_deg_c = self._get_temp()
print("write temp %.2f degc" % temp_
deg_c);
self._ble.gatts_write(self._handle,
struct.pack("
handle, self._handle)
if indicate:
# Indicate connected
centrals.
self._ble.gatts_
indicate(conn_handle, self._handle)
def _advertise(self, interval_us=500000):
self._ble.gap_advertise(interval_us, adv_
data=self._payload)
# ref https://github.com/raspberrypi/pico-
micropython-examples/blob/master/adc/temperature.
py
def _get_temp(self):
conversion_factor = 3.3 / (65535)
reading = self._sensor_temp.read_u16() *
conversion_factor
# The temperature sensor measures the Vbe
voltage of a biased bipolar diode, connected to
the fifth ADC channel
# Typically, Vbe = 0.706V at 27 degrees
C, with a slope of -1.721mV (0.001721) per
degree.
return 27 - (reading - 0.706) / 0.001721
def demo():
ble = bluetooth.BLE()
temp = BLETemperature(ble)
counter = 0
led = Pin('LED', Pin.OUT)
while True:
if counter % 10 == 0:
temp.update_temperature(notify=True,
indicate=False)
led.toggle()
time.sleep_ms(1000)
counter += 1
if __name__ == "__main__":
demo()
We’ll look at the code in a bit more detail shortly, but let’s first set up a computer to receive the data.
Thanks to modern web browsers’ ability to interact with BLE through the Web Bluetooth interface, you don’t need to install anything to get the data. Here’s example code for getting the Temperature characteristic from the Environmental Sensing service. Click on ’Start Notification’ and you should see a box pop up listing the available Bluetooth devices. Select the one starting ’Pico’ and you should (after a few seconds) see some data start to appear.
It might look a bit cryptic – there’ll be two sections starting with 0x. The number is a little-endian integer (the 0x at the start indicates that it’s being shown in hexadecimal format), so you need to convert it from the hex string shown. Remove both 0x figures, then paste the other digits into a converter like this one. Divide the result by 100, and you have the temperature of the Pico.
At this point, you could be entirely justified in wondering exactly how you are supposed to know that the number is a 2-bit little-endian integer. Fortunately, like most things Bluetooth related, it’s all in the documentation. In this case, it’s in the GATT Specification Supplement. Bluetooth is, in general, well-documented, but it’s a complex set of protocols with a lot of options, so finding the things you need in the massive pile of available documents can be a challenge. We’ll try to guide you to the appropriate places.
If this all seems a bit convoluted, it’s because Bluetooth really works best when it has specific code for both sending and receiving, but we’re using some generic code for receiving data. Since Pico can be a central device as well as a peripheral, there’s code for using a second Pico to receive the data, but we won’t look at that in detail in this article.
Digging into detail
Now that we can read the temperature, let’s go back and take a look at how this all works.
The first part of this code defines the identifiers we’re using:
_FLAG_READ = const(0x0002)
_FLAG_NOTIFY = const(0x0010)
_FLAG_INDICATE = const(0x0020)
# org.bluetooth.service.environmental_sensing
_ENV_SENSE_UUID = bluetooth.UUID(0x181A)
# org.bluetooth.characteristic.temperature
_TEMP_CHAR = (
bluetooth.UUID(0x2A6E),
_FLAG_READ | _FLAG_NOTIFY | _FLAG_INDICATE,
)
_ENV_SENSE_SERVICE = (
_ENV_SENSE_UUID,
(_TEMP_CHAR,),
)
# org.bluetooth.characteristic.gap.appearance.xml
_ADV_APPEARANCE_GENERIC_THERMOMETER = const(768)
Most of this is building up the _ENV_SENSE_SERVICE data, which is used as an input for gatts_register_services. This method is documented here. The key part of this is the single parameter which is (according to the documentation) “a list of services, where each service is a two-element tuple containing a UUID and a list of characteristics. Each characteristic is a two- or three-element tuple containing a UUID, a flags value, and, optionally, a list of descriptors.”
The available flags are then listed as:
_FLAG_BROADCAST = const(0x0001)
_FLAG_READ = const(0x0002)
_FLAG_WRITE_NO_RESPONSE = const(0x0004)
_FLAG_WRITE = const(0x0008)
_FLAG_NOTIFY = const(0x0010)
_FLAG_INDICATE = const(0x0020)
_FLAG_AUTHENTICATED_SIGNED_WRITE = const(0x0040)
_FLAG_AUX_WRITE = const(0x0100)
_FLAG_READ_ENCRYPTED = const(0x0200)
_FLAG_READ_AUTHENTICATED = const(0x0400)
_FLAG_READ_AUTHORIZED = const(0x0800)
_FLAG_WRITE_ENCRYPTED = const(0x1000)
_FLAG_WRITE_AUTHENTICATED = const(0x2000)
_FLAG_WRITE_AUTHORIZED = const(0x4000)
In this case, you can see that _ENV_SENSE_SERVICE is a two-element tuple containing first _ENV_SENSE_UUID. We’ve previyously defined this as 0x181A, which is listed in the document as Environmental Sensing Service. In the same document (section 6.1), it lists the range of allowable characteristics for this service. We can pick and choose any of these for our particular implementation, and we’ve selected temperature (defined in the same document as 0x2A6E).
The final set of constants:
_IRQ_CENTRAL_CONNECT = const(1)
_IRQ_CENTRAL_DISCONNECT = const(2)
_IRQ_GATTS_INDICATE_DONE = const(20)
are event codes from the MicroPython Bluetooth module. They are detailed here.
That’s the basic data you need to create a Bluetooth temperature controller. Let’s now take a more detailed look at the code.
Let’s work backwards from the demo method that’s kicked off when we run the script. This creates a BLETemperature object called temp. By creating it, this kicks off the __init__ method which sets everything up.
self._ble.irq(self._irq)
((self._handle,),) = self._ble.gatts_
register_services((_ENV_SENSE_SERVICE,))
self._connections = set()
if len(name) == 0:
name = 'Pico %s' % ubinascii.
hexlify(self._ble.config('mac')[1],':').decode().
upper()
print('Sensor name %s' % name)
self._payload = advertising_payload(
name=name, services=[_ENV_SENSE_UUID]
)
self._advertise()
The first line here tells the Bluetooth module to call the object’s _irq method when any event happens. This lets us handle things such as connections, disconnections, and if a central device has responded to us sending data with indicate.
After this, it sets the relevant data for the Bluetooth module and finally calls _advertise which itself just runs:
self._ble.gap_advertise(interval_us, adv_
data=self._payload)
This obviously starts advertising. It’s this that makes the device available for pairing. When you tried to read the temperature from your web browser, you would have seen a pop-up with the available devices. This, in essence, just means the devices that are currently advertising.
Once we’ve started this, we can get back to our demo method. From this point on, we can kind of ignore most of the Bluetooth stuff – it doesn’t really bother us. Advertising and pairing all happen in the background. We loop through and update the temperature using the method in the class:
def update_temperature(self, notify=False,
indicate=False):
# Write the local value, ready for a
central to read.
temp_deg_c = self._get_temp()
print("write temp %.2f degc" % temp_deg_c);
self._ble.gatts_write(self._handle,
struct.pack("
handle, self._handle)
if indicate:
# Indicate connected centrals.
self._ble.gatts_
indicate(conn_handle, self._handle)
The first part of this is just getting the data in the right format, which, as we’ve looked at previously, is two little-endian 2-bit numbers that, when combined together, give the temperature in 100ths of a degree Celsius.
The format string “
Sending the data is done in two parts. First, we write to the handle (we got the handle when we initialised the service in the __init__ method), and then either notify or indicate. There’s no difference between the two at this point, but if we did indicate, there would then be an event when the central confirmed it had received the data (see box). This code uses notify (it’s set in the demo method), but would work equally well using indicate.