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.

pico w and pi 400 lifestyle shot on wooden table

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.

pico w close up of metal enclosure holding wireless magic
The metal enclosure holds in the wireless magic

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.

The temperature data from the Pico W on-board sensor is often a few degrees too hot
The temperature data from the Pico W on-board sensor is often a few degrees too hot

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.

The official MicroPython examples guide you through using the hardware
The official MicroPython examples guide
you through using the hardware

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.

pico w face on
Pico W is an almost drop-in replacement for Pico but with Wireless LAN and Bluetooth

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 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.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Server Status

Aradippou Chat Larnaca Nicosia

Chat Links

Official Links.

Chat.

Alternative Mirror Links.

1. KiwiIRC 1.
2. KiwiIRC 2.

Other Web Clients.

1. IrcCloud.

Recent Posts

Related Posts:

Archives

Follow me on Mastodon

Super Club Radio

Mighty Deals

CyIRC

CyIRC Tweets

Chat Icon