Project Overview
Raspberry Pi Pico W Bluetooth Low Energy (BLE) with MicroPython: In this getting-started guide you will configure a Raspberry Pi Pico W (or Pico 2 W) as a BLE peripheral that streams its on-board temperature, then build a second Pico into a BLE central device that scans for, connects to, and reads from it using MicroPython and the aioble library.
- Time: 60-90 minutes
- Skill level: Intermediate
- What you will build: A BLE peripheral that advertises a temperature characteristic and a BLE central client that reads it over Bluetooth Low Energy.
Parts List
From ShillehTek
- Raspberry Pi Pico 2 W with Pre-Soldered Headers - you need two boards if you want to follow both the peripheral and the central device examples. One board is enough for the peripheral-only walkthrough.
- BME280 - optional add-on sensor if you want to publish real environmental readings (humidity and pressure) instead of the on-board temperature.
- SSD1306 OLED - optional display for showing readings on the central device without a computer.
- LCD1602 - optional character display alternative for showing readings on the central device.
External
- USB cable (Micro USB or USB-C, depending on your Pico revision) for programming
- Computer with Thonny IDE installed and the latest MicroPython firmware flashed to your board
- Smartphone with the nRF Connect for Mobile app from Nordic Semiconductor (Android or iOS)
Note: Bluetooth on the Raspberry Pi Pico requires a "W" board - either the Raspberry Pi Pico W or the newer Raspberry Pi Pico 2 W. Standard Pico and Pico 2 boards do not include the Infineon CYW43439 wireless chip and cannot follow this tutorial.
Step-by-Step Guide
Step 1 - Install the aioble Package
Goal: Get the recommended BLE library onto your Raspberry Pi Pico so the example code can run.
What to do: Connect your Pico W to your computer over USB and open Thonny IDE. From the menu bar choose Tools → Manage Packages…, type aioble into the search box, click the matching result, and press Install. Wait a few seconds for the package to land on the board.
Expected result: aioble is installed on the Pico, and you can import aioble from any MicroPython script running on the board.
Step 2 - Pick the Service and Characteristic UUIDs
Goal: Decide which BLE service and characteristic the peripheral will expose.
What to do: The peripheral will publish the on-board temperature, so we will reuse the SIG-assigned Environmental Sensing Service together with its standard Temperature characteristic. To find the UUIDs, open the Bluetooth SIG Assigned Numbers document. Search for "Environmental Sensing" to find the service and the supported characteristics (including temperature).
The Environmental Sensing Service short UUID is 0x181A:
And the temperature characteristic UUID is 0x2A6E:
Expected result: You have both UUIDs noted: service 0x181A, characteristic 0x2A6E.
Step 3 - Code the Pico W as a BLE Peripheral
Goal: Run a script on the first Pico W that registers the GATT service, writes the on-board temperature into the temperature characteristic every second, and continuously advertises the device under the name RPi-Pico.
What to do: Copy the script below into Thonny and run it on your first Pico W. The on-board temperature comes from the Pico's internal sensor through the picozero package, so make sure that package is installed too.
Code:
# Based on the Random Nerd Tutorials BLE example for the Raspberry Pi Pico W
# Project reference: https://RandomNerdTutorials.com/raspberry-pi-pico-w-bluetooth-low-energy-micropython/
from micropython import const
import asyncio
import aioble
import bluetooth
import struct
from picozero import pico_temp_sensor
# 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)
# Encode the temperature characteristic value (sint16, hundredths of a degree).
def _encode_temperature(temp_deg_c):
return struct.pack("<h", int(temp_deg_c * 100))
# Read the on-board temperature and update the characteristic.
async def sensor_task():
while True:
temperature = pico_temp_sensor.temp
temp_characteristic.write(_encode_temperature(temperature), send_update=True)
print(temperature)
await asyncio.sleep_ms(1000)
# Advertise the service. Pause advertising while a central is connected.
async def peripheral_task():
while True:
try:
async with await aioble.advertise(
_ADV_INTERVAL_MS,
name="RPi-Pico",
services=[_ENV_SENSE_UUID],
appearance=_ADV_APPEARANCE_GENERIC_THERMOMETER,
) as connection:
print("Connection from", connection.device)
await connection.disconnected()
except asyncio.CancelledError:
print("Peripheral task cancelled")
except Exception as e:
print("Error in peripheral_task:", e)
finally:
await asyncio.sleep_ms(100)
# 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())
This script is adapted from the official aioble temperature example.
Expected result: The Thonny shell prints temperature values once a second, and the Pico continuously advertises a BLE device named RPi-Pico with the Environmental Sensing service.
Step 4 - Verify with the nRF Connect App
Goal: Confirm the peripheral works by reading its temperature characteristic from your phone before you write any client code.
What to do: Install nRF Connect for Mobile from Nordic Semiconductor on your Android or iOS device. Open the app and start scanning for nearby BLE devices.
You should see the RPi-Pico device appear in the scan list. Tap Connect.
Once connected, expand the Environmental Sensing service to reveal the temperature characteristic. Use the arrow buttons to read the current value and enable notifications.
The raw bytes are not very useful by themselves, so switch the format using the small icon next to the value. On iPhone the second icon on the left opens the format picker.
Pick Unsigned Int. The temperature is encoded as Celsius multiplied by 100, so a reading of 2417 means 24.17 °C.
Expected result: nRF Connect shows updated temperature readings from the Pico W roughly every second. If you see this, the peripheral side of the project is working.
Step 5 - Code the Second Pico W as a BLE Central Device
Goal: Build a second Pico into a BLE client that scans for the RPi-Pico peripheral, connects to it, and prints the decoded temperature in the Thonny shell.
What to do: Connect your second Raspberry Pi Pico W (or 2 W) and copy the script below into a fresh Thonny instance pointed at that board.
Code:
# Based on the Random Nerd Tutorials BLE central device example for the Pico W
# Project reference: https://RandomNerdTutorials.com/raspberry-pi-pico-w-bluetooth-low-energy-micropython/
from micropython import const
import uasyncio as asyncio
import aioble
import bluetooth
import struct
# org.bluetooth.service.environmental_sensing
_ENV_SENSE_UUID = bluetooth.UUID(0x181A)
# org.bluetooth.characteristic.temperature
_ENV_SENSE_TEMP_UUID = bluetooth.UUID(0x2A6E)
# Name of the peripheral you want to connect to.
peripheral_name = "RPi-Pico"
# Decode the temperature characteristic encoding (sint16, hundredths of a degree).
def _decode_temperature(data):
try:
if data is not None:
return struct.unpack("<h", data)[0] / 100
except Exception as e:
print("Error decoding temperature:", e)
return None
async def find_temp_sensor():
# Active scan for 5 seconds with a tight interval/window for fast detection.
async with aioble.scan(5000, interval_us=30000, window_us=30000, active=True) as scanner:
async for result in scanner:
print(result.name())
if result.name() == peripheral_name and _ENV_SENSE_UUID in result.services():
return result.device
return None
async def main():
while True:
device = await find_temp_sensor()
if not device:
print("Temperature sensor not found. Retrying...")
await asyncio.sleep_ms(5000)
continue
try:
print("Connecting to", device)
connection = await device.connect()
except asyncio.TimeoutError:
print("Timeout during connection. Retrying...")
await asyncio.sleep_ms(5000)
continue
async with connection:
try:
temp_service = await connection.service(_ENV_SENSE_UUID)
temp_characteristic = await temp_service.characteristic(_ENV_SENSE_TEMP_UUID)
except asyncio.TimeoutError:
print("Timeout discovering services/characteristics. Retrying...")
await asyncio.sleep_ms(5000)
continue
while True:
try:
temp_data = await temp_characteristic.read()
if temp_data is not None:
temp_deg_c = _decode_temperature(temp_data)
if temp_deg_c is not None:
print("Temperature: {:.2f}".format(temp_deg_c))
else:
print("Invalid temperature data")
else:
print("Error reading temperature: None")
except Exception as e:
print("Error in main loop:", e)
break
await asyncio.sleep_ms(1000)
# Create the event loop and start the main task.
loop = asyncio.get_event_loop()
loop.create_task(main())
try:
loop.run_forever()
except Exception as e:
print('Error occurred: ', e)
except KeyboardInterrupt:
print('Program Interrupted by the user')
Expected result: The script saved successfully on the second Pico, ready to run.
Step 6 - Run Two Thonny Instances at Once
Goal: Have both Picos running simultaneously so you can watch the conversation happen in two shells.
What to do: By default Thonny only allows a single instance. To open two at once, go to Tools → Options and uncheck Allow only single Thonny instance. Close Thonny so the change takes effect, then reopen it twice. Make sure each window is connected to the correct COM/USB port using the picker in the bottom right corner.
Expected result: Two Thonny windows, each connected to a different Pico W on a different COM port.
Step 7 - Run Peripheral and Central Together
Goal: Watch the second Pico discover the first, connect, and print decoded temperature readings in real time.
What to do: First run the peripheral script on the first Pico W. Confirm it is advertising and printing temperature values. Then run the central device script in the second Thonny window on your second Pico W. The central will scan, find the RPi-Pico device, connect to it, discover the environmental sensing service, and start polling the temperature characteristic.
Expected result: The central device's Thonny shell prints lines like Temperature: 24.31 once a second, sourced from the other board over Bluetooth Low Energy.
Conclusion
You now have a working BLE setup on the Raspberry Pi Pico: one board acts as a peripheral that exposes an Environmental Sensing service, and a second board acts as a central device that discovers it and reads live temperature readings over Bluetooth Low Energy using MicroPython and aioble.
Want the exact parts used in this build? Grab them from ShillehTek.com. If you want help customizing this project or building something for your product, check out our IoT consulting services.


