ENG | Raspberry Pi Pico W, testing Bluetooth LE
Quick demonstration of BLE sensor implementations on Raspberry Pi Pico W using MicroPython
Disclaimer: This article and Python scripts were almost entirely generated by large language models. It exists for the backup and future reference, as these simple programs do not fit into git repository. I just needed some rapid prototypes and to learn basics about Bluetooth Low Energy (BLE)
This post demonstrates two practical implementations of Bluetooth Low Energy (BLE) on a Raspberry Pi Pico W, each suitable for different applications:
GATT-Based Connection: For reliable, two-way, connection-oriented communication.
Advertising Beacon: For lightweight, broadcast-only scenarios with low power consumption.
Both implementations simulate environmental sensor data (temperature, humidity, pressure).
Prerequisites
- Raspberry Pi Pico W with MicroPython firmware
- A host system with Bluetooth LE support (tested on Linux; should work on Windows/macOS with compatible drivers and libraries)
- Python 3.7+ with
bleak
library:pip install bleak
- USB Bluetooth adapter if your system lacks built-in BLE support
- nRF Connect for Mobile can be useful for diagnostic
Version 1: GATT-Based sensor service
Sensor simulation running on Raspberry Pi Pico W
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
import time
import bluetooth
import struct
from micropython import const
# BLE event constants
_IRQ_CENTRAL_CONNECT = const(1)
_IRQ_CENTRAL_DISCONNECT = const(2)
# Standard BLE UUIDs for Environmental Sensing
# This ensures compatibility with generic BLE scanner apps
_ENV_SENSE_UUID = bluetooth.UUID(0x181A)
_TEMP_CHAR_UUID = bluetooth.UUID(0x2A6E) # Temperature Characteristic
_HUM_CHAR_UUID = bluetooth.UUID(0x2A6F) # Humidity Characteristic
_PRESS_CHAR_UUID= bluetooth.UUID(0x2A6D) # Pressure Characteristic
# Flags for read and notify
_FLAG_READ = const(0x0001)
_FLAG_NOTIFY= const(0x0010)
# Prepare the GATT server structure
environment_sense_service = (
_ENV_SENSE_UUID,
(
(_TEMP_CHAR_UUID, _FLAG_READ | _FLAG_NOTIFY),
(_HUM_CHAR_UUID, _FLAG_READ | _FLAG_NOTIFY),
(_PRESS_CHAR_UUID, _FLAG_READ | _FLAG_NOTIFY),
),
)
# Advertisement helper for BLE
def advertise(ble, name="PICOW-02"):
# A simple advertisement for discoverability
# Including flags and name
adv_data = bytearray(
b"\x02\x01\x06" # Flags indicating BLE general discoverable
+ bytes([len(name) + 1, 0x09]) # Complete local name
+ name.encode()
)
ble.gap_advertise(100_000, adv_data)
class BLEEnvSense:
def __init__(self, ble):
self._counter = 0
self._ble = ble
self._ble.active(True)
self._ble.irq(self._irq)
# Holds the connection handle when a central device is connected
self._conn_handle = None
# Create a GATT server and register the Environmental Sensing service
((self._temp_handle, self._hum_handle, self._press_handle),) = self._ble.gatts_register_services((environment_sense_service,))
# Start advertising
advertise(self._ble)
def _irq(self, event, data):
if event == _IRQ_CENTRAL_CONNECT:
# A central connected
conn_handle, addr_type, addr = data
self._conn_handle = conn_handle
elif event == _IRQ_CENTRAL_DISCONNECT:
# A central disconnected
conn_handle, addr_type, addr = data
if conn_handle == self._conn_handle:
self._conn_handle = None
def update_values(self):
# Trial-error fixed point values found values for -12.34C, 43.21% RH, 101325.0Pa
temp_bytes = struct.pack('<i', -1234 + self._counter)
hum_bytes = struct.pack('<i', 4321 + self._counter)
press_bytes= struct.pack('<i', 1013250 + self._counter)
self._counter += 1
self._ble.gatts_write(self._temp_handle, temp_bytes)
self._ble.gatts_write(self._hum_handle, hum_bytes)
self._ble.gatts_write(self._press_handle, press_bytes)
# Notify only if there is a valid connection
if self._conn_handle is not None:
self._ble.gatts_notify(self._conn_handle, self._temp_handle)
self._ble.gatts_notify(self._conn_handle, self._hum_handle)
self._ble.gatts_notify(self._conn_handle, self._press_handle)
def main():
counter=0
ble = bluetooth.BLE()
print("BLE created")
env_sense = BLEEnvSense(ble)
print("BLEEnvSense created")
while True:
# Update BLE characteristics with constants for demonstration
env_sense.update_values()
# Delay before updating again
time.sleep(5)
if __name__ == "__main__":
main()
Testing on Linux
This requires some USB Bluetooth adapter with Bluetooth low energy.
BLE can’t scan and stay connected simultaneously in typical stacks. For example when nRF Connect on mobile reads sensor data, scan won’t discover it.
To verify your Bluetooth adapter:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
[pavel@marten -=- /home/pavel/dev-blog]$ lsusb
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 001 Device 011: ID 2357:0604 TP-Link TP-Link UB500 Adapter
Bus 002 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub
[pavel@marten -=- /home/pavel/dev-py/ble-server]$ bluetoothctl list
Controller E8:48:B8:C8:20:00 marten [default]
[pavel@marten -=- /home/pavel/dev-py/ble-server]$ btmgmt info
Index list with 1 item
hci0: Primary controller
addr E8:48:B8:C8:20:00 version 10 manufacturer 93 class 0x7c0104
supported settings: powered connectable fast-connectable discoverable bondable link-security ssp br/edr le advertising secure-conn debug-keys privacy static-addr phy-configuration wide-band-speech
current settings: powered bondable ssp br/edr le secure-conn wide-band-speech
name marten
short name
[pavel@marten -=- /home/pavel/dev-py/ble-server]$ bluetoothctl
Agent registered
[bluetoothctl]> scan le
SetDiscoveryFilter success
Discovery started
[CHG] Controller E8:48:B8:C8:20:00 Discovering: yes
[NEW] Device 2C:CF:67:EF:1B:9B PICOW-02
[NEW] Device 54:48:E6:79:4B:1E 54-48-E6-79-4B-1E
[NEW] Device 1F:CB:79:74:E8:74 1F-CB-79-74-E8-74
[NEW] Device 65:65:32:FA:16:D7 Apple Pencil
[NEW] Device 00:68:82:1B:CE:8D Intezze CLIQ
[NEW] Device 7C:43:73:5B:A2:EE 7C-43-73-5B-A2-EE
[CHG] Device 3E:5F:5B:C4:74:B5 RSSI: 0xffffffb6 (-74)
[CHG] Device 2C:CF:67:EF:1B:9B RSSI: 0xffffffb0 (-80)
[CHG] Device 54:48:E6:79:4B:1E RSSI: 0xffffffac (-84)
[CHG] Device 29:6F:8D:2A:80:3A RSSI: 0xffffff9e (-98)
[CHG] Device 54:48:E6:79:4B:1E RSSI: 0xffffffac (-84)
[CHG] Device 66:B3:E5:09:F3:EA RSSI: 0xffffff9a (-102)
[NEW] Device DE:27:B9:79:21:22 JBL Tune 520BT-LE
[CHG] Device 66:B3:E5:09:F3:EA RSSI: 0xffffff9c (-100)
[CHG] Device 66:B3:E5:09:F3:EA Name: JBL Tune 520BT-LE
[CHG] Device 66:B3:E5:09:F3:EA Alias: JBL Tune 520BT-LE
[CHG] Device 05:0C:55:50:CD:3A RSSI: 0xffffffa0 (-96)
[NEW] Device C5:E5:E2:0A:8F:1E MAJOR V [LE]
Reading sensor data in Python
1
pip install bleak
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
#!/usr/bin/env python3
import asyncio
from bleak import BleakClient, BleakScanner
import struct
import time
# UUIDs from your Pico W code
ENV_SENSE_UUID = "0000181a-0000-1000-8000-00805f9b34fb"
TEMP_CHAR_UUID = "00002a6e-0000-1000-8000-00805f9b34fb"
HUM_CHAR_UUID = "00002a6f-0000-1000-8000-00805f9b34fb"
PRESS_CHAR_UUID = "00002a6d-0000-1000-8000-00805f9b34fb"
class PicoWClient:
def __init__(self):
self.client = None
self.device_address = None
async def scan_for_device(self, device_name="PICOW-02", timeout=10):
"""Scan for the Pico W device"""
print(f"Scanning for device '{device_name}'...")
devices = await BleakScanner.discover(timeout=timeout)
for device in devices:
if device.name == device_name:
print(f"Found device: {device.name} ({device.address})")
self.device_address = device.address
return True
print(f"Device '{device_name}' not found")
return False
def notification_handler(self, sender, data):
"""Handle notifications from characteristics"""
# Convert UUID handle back to readable format
char_uuid = str(sender.uuid).lower()
if len(data) >= 4:
# Unpack as signed 32-bit integer (little endian)
value = struct.unpack('<i', data[:4])[0]
if TEMP_CHAR_UUID.lower() in char_uuid:
temp_celsius = value / 100.0
print(f"Temperature: {temp_celsius:.2f}°C (raw: {value})")
elif HUM_CHAR_UUID.lower() in char_uuid:
humidity = value / 100.0
print(f"Humidity: {humidity:.2f}% (raw: {value})")
elif PRESS_CHAR_UUID.lower() in char_uuid:
pressure = value / 10.0
print(f"Pressure: {pressure:.1f}Pa (raw: {value})")
async def connect_and_monitor(self):
"""Connect to device and start monitoring"""
if not self.device_address:
print("No device address available")
return
async with BleakClient(self.device_address) as client:
self.client = client
print(f"Connected to {self.device_address}")
# Subscribe to notifications for all characteristics
await client.start_notify(TEMP_CHAR_UUID, self.notification_handler)
await client.start_notify(HUM_CHAR_UUID, self.notification_handler)
await client.start_notify(PRESS_CHAR_UUID, self.notification_handler)
print("Subscribed to notifications. Waiting for data...")
try:
# Keep the connection alive and listen for notifications
while True:
await asyncio.sleep(1)
except KeyboardInterrupt:
print("\nDisconnecting...")
await client.stop_notify(TEMP_CHAR_UUID)
await client.stop_notify(HUM_CHAR_UUID)
await client.stop_notify(PRESS_CHAR_UUID)
async def main():
client = PicoWClient()
# Scan for the device
if await client.scan_for_device():
# Connect and start monitoring
await client.connect_and_monitor()
else:
print("Could not find the Pico W device")
if __name__ == "__main__":
asyncio.run(main())
1
2
chmod +x main.py
./main.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Scanning for device 'PICOW-02'...
Found device: PICOW-02 (2C:CF:67:EF:1B:9B)
Connected to 2C:CF:67:EF:1B:9B
Subscribed to notifications. Waiting for data...
Temperature: -9.56°C (raw: -956)
Humidity: 45.99% (raw: 4599)
Pressure: 101352.8Pa (raw: 1013528)
Temperature: -9.55°C (raw: -955)
Humidity: 46.00% (raw: 4600)
Pressure: 101352.9Pa (raw: 1013529)
Temperature: -9.54°C (raw: -954)
Humidity: 46.01% (raw: 4601)
Pressure: 101353.0Pa (raw: 1013530)
^C
Version 2: Advertising Beacon Only
A simpler, connectionless approach where the Pico W periodically advertises sensor data encoded in an advertising packet.
Rationale
Advantages:
- Multiple scanners can receive broadcasts simultaneously.
- Lower power usage by avoiding persistent connections.
Limitations:
- BLE advertising payloads are limited to 31 bytes, which constrains how much sensor data and metadata can be included.
- WARNING: Deep sleep on the Pico is incompatible with USB serial connections and will require to erase flash memory and reflashing MicroPython.
Raspberry Pico W code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
import time
import bluetooth
import struct
import machine
from micropython import const
# Custom service UUID for our sensor beacon
_SENSOR_BEACON_UUID = 0x1234
class BLESensorBeacon:
def __init__(self, ble, device_name="SENSOR-BEACON"):
self._ble = ble
self._ble.active(True)
self._device_name = device_name
self._counter = 0
def _create_adv_data(self, temp, humidity, pressure):
"""Create advertising data with sensor values"""
name = self._device_name
# Pack sensor data: temp (2 bytes), humidity (2 bytes), pressure (4 bytes)
# Using signed 16-bit for temp/humidity, signed 32-bit for pressure
sensor_data = struct.pack('<hhI', temp, humidity, pressure)
# Build advertising packet
adv_data = bytearray()
# Flags (standard BLE discoverable flags)
adv_data.extend(b"\x02\x01\x06")
# Complete local name
adv_data.extend(bytes([len(name) + 1, 0x09]))
adv_data.extend(name.encode())
# Custom service data with our sensor readings
service_data = struct.pack('<H', _SENSOR_BEACON_UUID) + sensor_data
adv_data.extend(bytes([len(service_data) + 1, 0x16]))
adv_data.extend(service_data)
return adv_data
def advertise_sensor_data(self, temp, humidity, pressure):
"""Advertise sensor data in beacon format"""
adv_data = self._create_adv_data(temp, humidity, pressure)
# Advertise for 1 second, then stop
self._ble.gap_advertise(1000_000, adv_data) # 1 second in microseconds
print(f"Broadcasting: T={temp/100:.2f}°C, H={humidity/100:.2f}%, P={pressure/10:.1f}Pa")
time.sleep(1) # Keep advertising active for 1 second
self._ble.gap_advertise(None) # Stop advertising
def deep_sleep(self, seconds):
"""Enter deep sleep to save power"""
print(f"Entering deep sleep for {seconds} seconds...")
machine.deepsleep(seconds * 1000) # Convert to milliseconds
def main():
ble = bluetooth.BLE()
beacon = BLESensorBeacon(ble)
counter = 0
while True:
# Simulate sensor readings (same as your original code)
temp = -1234 + counter # Represents -12.34°C + counter
humidity = 4321 + counter # Represents 43.21% + counter
pressure = 1013250 + counter # Represents 101325.0Pa + counter
# Broadcast sensor data
beacon.advertise_sensor_data(temp, humidity, pressure)
counter += 1
# For ultra-low power: use deep sleep instead of regular sleep
# beacon.deep_sleep(60) # Sleep for 60 seconds
# For testing: use regular sleep (easier to interrupt)
print("Sleeping for 10 seconds...")
time.sleep(10)
if __name__ == "__main__":
main()
1
2
3
4
5
6
7
8
9
10
MicroPython v1.25.0 on 2025-04-15; Raspberry Pi Pico 2 W with RP2350
Type "help()" for more information or .help for custom vREPL commands.
>>>
Broadcasting: T=-12.34°C, H=43.21%, P=101325.0Pa
Sleeping for 10 seconds...
Broadcasting: T=-12.33°C, H=43.22%, P=101325.1Pa
Sleeping for 10 seconds...
Broadcasting: T=-12.32°C, H=43.23%, P=101325.2Pa
Sleeping for 10 seconds...
Linux code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#!/usr/bin/env python3
import asyncio
from bleak import BleakScanner
import struct
import time
# Custom service UUID from beacon
SENSOR_BEACON_UUID = 0x1234
class BeaconListener:
def __init__(self, device_name="SENSOR-BEACON"):
self.device_name = device_name
self.last_data = {}
def parse_sensor_data(self, advertisement_data):
"""Parse sensor data from advertisement"""
service_data = advertisement_data.service_data
# Look for our custom service UUID
service_uuid = f"0000{SENSOR_BEACON_UUID:04x}-0000-1000-8000-00805f9b34fb"
if service_uuid in service_data:
data = service_data[service_uuid]
if len(data) >= 8: # 2+2+4 bytes for temp, humidity, pressure
temp, humidity, pressure = struct.unpack('<hhI', data[:8])
return {
'temperature': temp / 100.0,
'humidity': humidity / 100.0,
'pressure': pressure / 10.0,
'timestamp': time.time()
}
return None
def detection_callback(self, device, advertisement_data):
"""Called when a BLE device is detected"""
if device.name == self.device_name:
sensor_data = self.parse_sensor_data(advertisement_data)
if sensor_data:
# Check if this is new data (avoid duplicates)
data_key = f"{sensor_data['temperature']}-{sensor_data['humidity']}-{sensor_data['pressure']}"
if data_key not in self.last_data or time.time() - self.last_data[data_key] > 5:
print(f"[{time.strftime('%H:%M:%S')}] {device.name} ({device.address}):")
print(f" Temperature: {sensor_data['temperature']:.2f}°C")
print(f" Humidity: {sensor_data['humidity']:.2f}%")
print(f" Pressure: {sensor_data['pressure']:.1f}Pa")
print(f" RSSI: {advertisement_data.rssi} dBm")
print()
self.last_data[data_key] = time.time()
async def start_listening(self):
"""Start scanning for beacon advertisements"""
print(f"Listening for beacon advertisements from '{self.device_name}'...")
print("Press Ctrl+C to stop\n")
scanner = BleakScanner(detection_callback=self.detection_callback)
try:
await scanner.start()
# Keep scanning until interrupted
while True:
await asyncio.sleep(1)
except KeyboardInterrupt:
print("\nStopping scanner...")
finally:
await scanner.stop()
async def main():
listener = BeaconListener()
await listener.start_listening()
if __name__ == "__main__":
asyncio.run(main())
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
[pavel@marten -=- /home/pavel/dev-py/ble-server]$ ./listen-beacon.py
Listening for beacon advertisements from 'SENSOR-BEACON'...
Press Ctrl+C to stop
[11:20:41] SENSOR-BEACON (2C:CF:67:EF:1B:9B):
Temperature: -12.34°C
Humidity: 43.21%
Pressure: 101325.0Pa
RSSI: -84 dBm
[11:21:14] SENSOR-BEACON (2C:CF:67:EF:1B:9B):
Temperature: -12.31°C
Humidity: 43.24%
Pressure: 101325.3Pa
RSSI: -84 dBm
[11:21:25] SENSOR-BEACON (2C:CF:67:EF:1B:9B):
Temperature: -12.30°C
Humidity: 43.25%
Pressure: 101325.4Pa
RSSI: -86 dBm
[11:21:58] SENSOR-BEACON (2C:CF:67:EF:1B:9B):
Temperature: -12.27°C
Humidity: 43.28%
Pressure: 101325.7Pa
RSSI: -84 dBm
[11:22:09] SENSOR-BEACON (2C:CF:67:EF:1B:9B):
Temperature: -12.26°C
Humidity: 43.29%
Pressure: 101325.8Pa
RSSI: -96 dBm
[11:22:21] SENSOR-BEACON (2C:CF:67:EF:1B:9B):
Temperature: -12.25°C
Humidity: 43.30%
Pressure: 101325.9Pa
RSSI: -68 dBm
[11:22:32] SENSOR-BEACON (2C:CF:67:EF:1B:9B):
Temperature: -12.24°C
Humidity: 43.31%
Pressure: 101326.0Pa
RSSI: -66 dBm
[11:22:44] SENSOR-BEACON (2C:CF:67:EF:1B:9B):
Temperature: -12.23°C
Humidity: 43.32%
Pressure: 101326.1Pa
RSSI: -76 dBm
Note
Data are transmitted three times rather than once for a second. This is hopefully more reliable. But because this is not connection oriented, reliable data reception cannot be guaranteed.
Comparison
Feature | GATT Connection | Beacon Advertising |
---|---|---|
Power Consumption | Higher (continuous connection) | Ultra-low (less than 5mA during sleep) |
Data Reliability | High (acknowledgments) | Variable (broadcast only) |
Multiple Clients | No (typically 1:1) | Yes (1:many) |
Payload Size | Large (up to 512 bytes) | Limited (31 bytes) |
Complexity | Higher (pairing, services) | Lower (just advertising) |
When to Use GATT-Based Approach
- Pros: Reliable data delivery, bidirectional communication, standard BLE service discovery
- Cons: Higher power consumption, single client connection, more complex pairing
- Use Cases: Interactive devices, configuration interfaces, continuous monitoring
When to Use Beacon Approach
- Pros: Ultra-low power consumption (under 10mA during sleep) , multiple simultaneous receivers, no pairing required
- Cons: 31-byte payload limit, no delivery confirmation, potential data loss
- Use Cases: Environmental monitoring, asset tracking, proximity detection
NOTE: Battery life and power consumption were not verified, Raspberry Pi Pico W is not suitable for low-power applications in the first place. Data loss in beacon mode is a real issue.
Conclussion
This quick experiment tested two distinct BLE approaches on the Raspberry Pi Pico W. The initial GATT-based implementation proved functional but revealed several limitations during testing — notably with persistent connections, discovery quirks, and the overhead of maintaining active connections in simple sensor scenarios.
The second, beacon-style approach was introduced as a practical alternative, addressing these issues by offering connectionless broadcasts, simplifying client-side data collection, and reducing complexity for low-power, multi-reader environments.
Both examples serve as practical prototypes for selecting the right BLE pattern depending on application constraints — connection-oriented for interactive control and data exchange, or connectionless for periodic sensor broadcasting.
Oh, wait, do these approaches need to be entirely exclusive?