Did you happen to get your hands onto the Raspberry Pi Pico W and wondered why it can’t use Bluetooth with micropython? Well now is the time to actually test this as it was officially announced that by now we should be able to! In this short announcement there are a few useful Links for us to get started. So, if you’re ready, grab a cup of coffee and let’s begin!
Tip: Only have a normal Pico? Here you can read another post about how to connect to other devices with a cheap radio module!
Getting Started with the Raspberry Pi Pico W
First of all, what needs to be said is that although the bluetooth features already were included into the official micropython github-project, we cannot simply use the newest Micropython-Firmware from the official releases, because the newest stable version still does not have these changes built-in at the moment. Luckily, in the above mentioned announcement-post there is a link to the official Raspberry Pi Pico W documentation page where we can also find a Micropython build on another documentation page that has all the necessary features already in there.
So go ahead and download the correct .uf2-File (where the link says “Raspberry Pi Pico W with Wi-Fi and Bluetooth LE support”). Once downloaded, just load it onto your Pico W like you probably already did before. If not, there is a short tutorial on how to do it on the very same page where we downloaded it.
Using BLE
Now that we have installed the correct Firmware, we can use the actual micropython bluetooth package like on any other BLE-enabled board (e.g. a esp32 board). Before we do the actual coding part, here is a short introduction about the essentials that we will need when we want to code something with BLE. Please feel free to skip this part if you are already familiar with the concepts described below.
Roles of the BLE-devices and Protocols they use
In order to exchange data between the devices, we will assign roles to them. In other words, we just use each of the devices in a specific way to make our lifes easier.
The Peripheral Role
In our example we will have one device acting as a Peripheral device, which will be responsible for sensing and providing temperature data. This device will use the GAP (Generic Access Profile) Protocol when it is not connected, in order to broadcast its profile out to other BLE-devices. So if this device is in broadcast-mode, this for us just means that other devices can discover this device.
Once a connection is established to the peripheral device, it will stop the broadcasting process and switch over to another commonly used Protocol, the GATT (Generic Attributes Profile) Protocol. More specifically it will act as a GATT-Server, since the peripheral holds and serves the data that is interesting to other devices.
The Central Device Role
We will also have another device within our example that is acting as a Central Device. Other than a peripheral, the central device is able to connect to multiple other devices at once. It will be responsible for discovery and connection to our peripheral device. Once it successfully established the connection to it, it will listen to the peripherals notifications. In this way it is also using the GATT Protocol acting as a GATT-Client.
GATT-Services and GATT-Characteristics
We will use a GATT-Characteristic as our temperature-data keeper so to say. Everytime we read our temperature-data (e.g. from a sensor), we will be able to write this data into the characteristics value store. This does not automatically mean that a potentially connected central device will get a notification each time we do that. If we want this to happen we can actually manually notify the central device on whatever condition is needed for that.
Other than that, a characteristic can also hold other information: The access permission flag, for example indicates whether a GATT-Client can read the current value or even write over it. Also we can describe in which type of format that the temperature is in (e.g. celcius). A Characteristic is always wrapped within a GATT-Service.
A GATT-Service can wrap one or multiple different GATT-Characteristics and is commonly used to describe which kind of data is to be awaited within its characteristics. In our example we will have one temperature characteristic wrapped into one environmental-sensing service. If for example we would have a temperature-sensor that would also be capable to sense humidity, then we could additionally attach another humidity-characteristic to our service (both temperature and humidity fits into our environmental-sensing service description).
Every service and also every characteristic needs to have an (16 or 128-bit long) identifier. There is a whole bunch of pre-defined and commonly used UUIDs out there for us to quickly describe our services and characteristics and we will also make use of those later in the code. They were developed by the Bluetooth Special Interest Group and can be found within this official document. They are very useful when we want to describe common types of services or characteristics (e.g. temperature). Just have look into the section “16-bit UUIDs” if you are interested.
Please note that it is completely up to us on how we define and manage characteristics and services. If we want to use our own UUID (e.g. to describe something that is not already described by the Bluetooth SIG), it is recommended to use 128-bit long identifiers.
Coding
Now that we understand the basics of BLE-Roles and the common protocols that are being used, we can now start creating our temperature-reading code example. I will use the Pico W as a Peripheral device (it will sense temperature data and serve it to a central device) by using the official micropython-lib package aioble. As the central device I will use my PC (you can basically use anything that runs Python 3 and has BLE-support) with Python 3 and the package bleak for using BLE. Both of these packages can be used in an asynchronous manner and they actually work pretty similar. In the next part I will show you how to install both aioble and bleak.
Installing aioble on the Pico W
Luckily we can connect to the internet with the Pico W and download this package via the micropython package-manager mip. Just copy and paste the following code onto your Pico W and replace the SSID and PW strings with your Wifi name and password:
import network
def do_connect():
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
if not wlan.isconnected():
print('connecting to network...')
wlan.connect('Your-SSID', 'Your-PW')
while not wlan.isconnected():
pass
print('network config:', wlan.ifconfig())
do_connect()
After replacing SSID and PW, just execute the script on your Pico W. If there is no error this probably means it successfully connected to your Wifi. For the next step execute the following in your Python REPL-Shell:
>>> import mip
>>> mip.install('aioble')
This will install the aioble package onto your Pico W. Let’s continue installing bleak on the PC.
Installing bleak on the PC
We can simply use the python package-manager pip to install bleak. Please note, the device that you want to use as a central device in this example must be able to use Python 3 and does need a BLE-enabled bluetooth module. If this is the case and also assuming that Python is already installed, just open a terminal on your machine and execute the following:
> python -m pip install bleak
Peripheral Code
In the aioble Github-Project we can find and use this example script and change it to our needs:
import sys
sys.path.append("")
from micropython import const
import uasyncio as asyncio
import aioble
import bluetooth
import random
import struct
# org.bluetooth.service.environmental_sensing
_ENV_SENSE_UUID = bluetooth.UUID(0x181A)
# org.bluetooth.characteristic.temperature
_ENV_SENSE_TEMP_UUID = bluetooth.UUID(0x2A6E)
# org.bluetooth.characteristic.gap.appearance.xml
_ADV_APPEARANCE_GENERIC_THERMOMETER = const(768)
# How frequently to send advertising beacons.
_ADV_INTERVAL_MS = 250_000
# Register GATT server.
temp_service = aioble.Service(_ENV_SENSE_UUID)
temp_characteristic = aioble.Characteristic(
temp_service, _ENV_SENSE_TEMP_UUID, read=True, notify=True
)
aioble.register_services(temp_service)
# Helper to encode the temperature characteristic encoding (sint16, hundredths of a degree).
def _encode_temperature(temp_deg_c):
return struct.pack("<h", int(temp_deg_c * 100))
# This would be periodically polling a hardware sensor.
async def sensor_task():
t = 24.5
while True:
temp_characteristic.write(_encode_temperature(t))
t += random.uniform(-0.5, 0.5)
await asyncio.sleep_ms(1000)
async def notify_gatt_client(connection):
if connection is None: return
temp_characteristic.notify(connection)
# Serially wait for connections. Don't advertise while a central is
# connected.
async def peripheral_task():
while True:
async with await aioble.advertise(
_ADV_INTERVAL_MS,
name="mpy-temp",
services=[_ENV_SENSE_UUID],
appearance=_ADV_APPEARANCE_GENERIC_THERMOMETER,
) as connection:
print("Connection from", connection.device)
while connection.is_connected():
await notify_gatt_client(connection)
await asyncio.sleep(5)
# Run both tasks.
async def main():
t1 = asyncio.create_task(sensor_task())
t2 = asyncio.create_task(peripheral_task())
await asyncio.gather(t1, t2)
asyncio.run(main())
I actually just altered the function def peripheral_task()
, so that it will notify the central device (if connected) every 5 seconds. Of course you are free to use any other condition here. The code first instanciates the service and characteristic objects, then adds the characterstic to the service and finally registers the service to the BLE-module via aioble.register_services(...)
The function def _encode_temperature(temp_deg_c)
is responsible for data serialization. The next function async def sensor_task()
is meant to be used as an async task. It has an infinite loop built into it, so that if we run our code, this will make sure that the temperature value will be refreshed every second.
The main function here is def peripheral_task()
which also will try running indefinitely. First it will start adverstising things like the bluetooth public name and address and also all the services that we provide within the aioble.advertise(services=[...])
services-parameter. We await it until a connection has been established with the central device. We can then refer to this device over the connection
variable. Then we will notify the other device every 5 seconds until the connection ends for some reason. If this happens, it will return back to do the advertising again, and again waiting for the central device to connect.
Central Device Code
For our central device we will use the following python script:
import asyncio
import logging
import struct
import bleak
logger = logging.getLogger(__name__)
# org.bluetooth.service.environmental_sensing
_ENV_SENSE_UUID = "0000{0:x}-0000-1000-8000-00805f9b34fb".format(0x181A)
# org.bluetooth.characteristic.temperature
_ENV_SENSE_TEMP_UUID = "0000{0:x}-0000-1000-8000-00805f9b34fb".format(0x2A6E)
def _decode_temperature(data):
return struct.unpack("<h", data)[0] / 100
def _callback(sender: bleak.BleakGATTCharacteristic, data: bytearray):
data = None if not data else _decode_temperature(data)
print(f"{sender}: {data}")
async def find_temp_sensor():
name = 'mpy-temp'
return await bleak.BleakScanner.find_device_by_name(name)
async def do_connect():
logger.info("Start scanning for temperature sensor device")
device = await find_temp_sensor()
if not device:
logger.error("Temperature sensor not found")
return
async with bleak.BleakClient(device) as client:
# Get the service:
service = client.services.get_service(_ENV_SENSE_UUID)
if service is None:
logger.error("Temperature service not found")
return
characteristic = service.get_characteristic(_ENV_SENSE_TEMP_UUID)
if characteristic is None:
logger.error("Temperature characteristic not found")
return
await client.start_notify(characteristic, _callback)
while client.is_connected:
await asyncio.sleep(5)
async def main():
while True:
await do_connect()
asyncio.run(main())
You might have noticed the different definition of the UUIDs above. Bleak actually has a helper-method for this to make it easier (like with the aioble-package in micropython), but this is not included yet in the latest release on the official pip-sources, so I did it this way.
def _decode_temperature(temp_deg_c)
is just a helper function for the deserialization of the temperature data when it is being received by the device. def _callback(sender, data)
defines the callback function for when the device is being notified on the temperature characteristic. def find_temp_sensor()
makes use of the BleakScanner
class in order to find our peripheral device by its name. Feel free to use any other discovery method which can be found here.
Finally the def do_connect()
function will first try to find the peripheral device, and then will try to find the right characteristic within the right service over the pre-defined UUIDs. Then it will hook the _callback
function to the notify event of this characteristic. When it successfully did that, the function will just do asyncio.sleep(5)
in order to run until the connection ends, just like we saw earlier in the peripheral code. If the connection ends, the function will be called again and everything of the above will be repeated.
Done! You can run both of the scripts at the same time on each of the devices and see if everything works as intended. Feel free to write a comment below if there are any question left.
Further Reading / Links
- Want to use another micropython-abled board for the central device? Have a look at the aioble-examples.
- I would recommend using asynchronous programming like with in the example above, but if you don’t want to, you can also use the more low-level ‘bluetooth’ package in micropython and you can use bleak in a non-async way: see documentation. You can find a low-level ble example in chapter 6 “Working with Bluetooth in Micropython” in the official Raspberry Pi “Connecting to the Internet with Raspberry Pi Pico W” Document