Getting to grips with Bluetooth on Pico W
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("<h", int(temp_deg_c * 100)))
if notify or indicate:
for conn_handle in self._connections:
if notify:
# Notify connected centrals.
self._ble.gatts_notify(conn_
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("<h", int(temp_deg_c * 100)))
if notify or indicate:
for conn_handle in self._connections:
if notify:
# Notify connected centrals.
self._ble.gatts_notify(conn_
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 “<h” is little-endian, 2-bit signed integers – the first character is the endianness, and the second is the number format (you can see a full list of other options here).
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.
18 comments
Jim Mussared
Thanks Ben, great article. If anyone finds this all a bit complicated though, I should note that the bluetooth module described here is the low-level interface and I strongly recommend using the aioble library instead. It makes common tasks much simpler. The library can be installed with the mip package manager, see details and examples here: https://github.com/micropython/micropython-lib/tree/master/micropython/bluetooth/aioble
John Ouellette
Excellent. Thanks Jim!
GarlicFella
Speaking of Bluetooth and the updated Pico W, can you give a rough estimation, when the updated book “Get Started with MicroPython on Raspberry Pi Pico” will be available?
Christoff Smith
Raspberry Pi Staff Liz Upton
Liz Upton previously wrote
14th June 2023, 12:09 pm
There will – it’s being written and edited at the moment. I don’t have an eta for you, but it’ll be available before the end of the year.
Nicko
Thanks for these helpful instructions!
There are a few places in the text where you refer to “2-bit integers”. 2-bit sign integers have a rather limited range from -2 to +1, inclusive, so I’m guessing that you meant to write “2 byte integers”!
Kalle
Thank you again!
It’s really great to see you support the current sevices already out and develop their capabilities.
Kalle
*devices
Dennis Atwood
Something not explicitly mentioned here is that pairing is still not supported. I spent literally days trying to make something I could connect to from my Android phone, then gave up and used an ESP32 with the Arduino IDE and was done in hours. I look forward to when BLE is fully implemented on Pico and it has as many code examples out there as you can find for ESP32.
Ed
> Thanks to modern web browsers’ ability to interact with BLE through the Web Bluetooth interface
Well, not browsers plural: only Chrome/Chromium. Firefox and Safari, also modern web browsers, do not support Web Bluetooth at all.
Doug Blanding
You’re right Ed. But thankfully, I happen to have 2 of these little picow devices so I was able to successfully use one to run the sensor code and the 2nd one to run the reader code and everything worked out perfectly. It’s nice that everything worked on the first try. Thanks Ben for posting this excellent example!
Nick Pettefar
I want to use one Pico W to control another with Bluetooth. In other words to build a controller and a device to be controlled, say battling tanks. Do you have instructions/details about that kind of use please?
It would be great if you had a Pico W book!
Mo
I am looking to do the exact same thing?!
SecretSource
The post should mention that it only works with “Nightly builds” and that the latest stable
v1.20.0(2023-04-26).uf2 will not work!
cme
THANK YOU!! THANK YOU!! secretsource.
Been beating my head on pico cuz bluetooth module wouldn’t import and this secret comment is literally the only answer !anywhere! and stumbled on it by chance
I’m disappointed that it seems pairing may be an issue but will give a go anyway (trying to connect to ELM327 BT OBD2)
RAGHAVENDRA RAO
Dear Ben
I have run the program as follows:
1. The file: ble_advertising.py is stored on the device(Pico W)
2. The file: picow_ble_temp_sensor.py is loaded in Thonny IDE and Run.
3. Web Bluetooth / Notifications Sample
is opened and presssed Start Notification
4. selected the paired device from the popup box.
5. Temperature is displayed at the Live Output
Thanks for sharing useful Info
Anthony
Holy man. Dude, someone needs to just write a library that allows you to use it like I2C but wirelessly. Why exactly does it have to be so absurdly convoluted? I2C works great with multiple devices etc. Or just an initial discovery/setup procedure to connect two devices and then it just acts like an uart thereafter! That would be great. If I even have to use Bluetooth, that’s exactly what I will have to do. Bus go in one end, come out the other. Clearly that’s where you have to be eventually anyway. It’s like a big jumble of parts instead of a working car.
jbeale
If I’m not mistaken, “classic” bluetooth does have a UART profile that could work the way you describe, although setup is maybe not as easy as you want. However the BLE (low energy) mode for devices like a temperature sensor is intentionally different from that. It only uses very short packets of data, signaled by the “Notify” method, to preserve remote sensor battery life. UARTs and I2C protocols were not originally designed to operate over a radio link where every transmit microamp counts.
Brian Reinhold
DO you know where I can get the C library for Bluetooth on the PICO w?
Comments are closed