The Bluetooth Low Energy (BLE) is a low-power, short-range wireless technology designed for exchanging small amounts of data.
While Wi-Fi forces you to be constantly “shouting” to maintain the connection (draining the battery), BLE is designed to sleep 99% of the time and wake up for only a few milliseconds to send a small piece of data. It’s the technology behind fitness trackers, door sensors, and tracking tags.
Let me be upfront from the start: programming BLE is more difficult than programming Wi-Fi. It’s not as simple as “connect and send.” There’s an underlying architecture we need to understand.
It’s Not Your Car’s Bluetooth
The first thing to clarify: BLE is not Bluetooth Classic.
- Bluetooth Classic: Designed for continuous data flow (Audio, file transfer).
- BLE (Low Energy): Designed for small data packets (temperature, button state) with incredibly low power consumption.
In MicroPython, we use the standard bluetooth module.
GAP: How the Device Advertises
GAP (Generic Access Profile) defines how devices find each other and connect. There are two main roles here:
- Peripheral: It’s the “small” device (your ESP32/Pico). It advertises saying “I’m here!” and waits for someone to connect.
- Central: It’s the “smart” device (your phone or tablet). It scans the environment looking for peripherals and initiates the connection.
The Advertising Process
Before connecting, the Peripheral sends Advertising packets. These are public messages that say: “My name is ESP32-Sensor and I offer a temperature service.”
GATT: How Data is Organized
Once connected, we enter the world of GATT (Generic Attribute Profile). This is where data is exchanged. GATT organizes information like a folder structure:
- Profile: The complete set.
- Service: Groups logical functionalities (e.g., “Battery Service”, “Heart Rate Service”).
- Characteristic: This is the actual data. It’s the variable we can read or write (e.g., “Battery Level %”, “Beats per minute”).
UUIDs: Bluetooth Identifiers
Each Service and Characteristic has a unique 128-bit identifier called a UUID.
- There are standard short UUIDs (16 bits) for common things (e.g.,
0x180Fis the Battery Service). - For our own projects, we generate random long UUIDs.
Let’s Get Hands-On: A BLE Peripheral in MicroPython
Let’s create a device that:
- Advertises itself as “Mi-ESP32”.
- Has its own Service.
- Has a Characteristic that allows us to turn an LED on/off by writing from a phone.
BLE code in MicroPython is “verbose” (a lot of code) because it’s low-level and event-driven (interrupts).
import bluetooth
import time
from machine import Pin
from micropython import const
# --- Definitions and Constants ---
# BLE system events (IRQ)
_IRQ_CENTRAL_CONNECT = const(1)
_IRQ_CENTRAL_DISCONNECT = const(2)
_IRQ_GATTS_WRITE = const(3)
# Define our UUIDs (Randomly generated)
# Service: 12345678-1234-5678-1234-56789abcdef0
_MI_SERVICIO_UUID = bluetooth.UUID("12345678-1234-5678-1234-56789abcdef0")
# Characteristic (LED): 12345678-1234-5678-1234-56789abcdef1
_MI_CHAR_UUID = bluetooth.UUID("12345678-1234-5678-1234-56789abcdef1")
# Permission flags: Can be read (READ) and written (WRITE)
_FLAG_READ = const(0x0002)
_FLAG_WRITE = const(0x0008)
_FLAG_NOTIFY = const(0x0010)
# --- BLE Peripheral Class ---
class BLELedPeripheral:
def __init__(self, ble, name="Mi-ESP32"):
self._ble = ble
self._ble.active(True)
self._ble.irq(self._irq_handler)
# Configure the LED
self._led = Pin(2, Pin.OUT)
self._led.value(0)
# Register the service and characteristic
# ((UUID, FLAGS),) -> Tuple of characteristics
SERVICES = (
(_MI_SERVICIO_UUID, ((_MI_CHAR_UUID, _FLAG_READ | _FLAG_WRITE),)),
)
# gatts_register_services returns the "handles"
# We need to store them to know which characteristic the phone is writing to
((self._handle,),) = self._ble.gatts_register_services(SERVICES)
self._conn_handle = None
self._payload = self._create_advertising_payload(name=name)
self._advertise()
def _irq_handler(self, event, data):
# This function runs automatically when something happens
if event == _IRQ_CENTRAL_CONNECT:
conn_handle, _, _ = data
print("Phone connected!", conn_handle)
self._conn_handle = conn_handle
elif event == _IRQ_CENTRAL_DISCONNECT:
conn_handle, _, _ = data
print("Phone disconnected.")
self._conn_handle = None
# Important: Re-advertise so another device can connect
self._advertise()
elif event == _IRQ_GATTS_WRITE:
conn_handle, value_handle = data
# Check if they wrote to our LED characteristic
if conn_handle == self._conn_handle and value_handle == self._handle:
# Read the value they wrote
val = self._ble.gatts_read(self._handle)
self._update_led(val)
def _update_led(self, data):
# If we receive a '1' or 'ON', turn the LED on
try:
texto = data.decode().strip()
print("Received:", texto)
if texto == "1" or texto == "ON":
self._led.value(1)
elif texto == "0" or texto == "OFF":
self._led.value(0)
except:
pass
def _advertise(self, interval_us=500000):
print("Advertising...")
self._ble.gap_advertise(interval_us, adv_data=self._payload)
# Helper function to create the advertising packet (GAP)
# This is technical; it basically packages the name into bytes
def _create_advertising_payload(self, limited_disc=False, br_edr=False, name=None, services=None, appearance=0):
import struct
payload = bytearray()
def _append(adv_type, value):
payload.append(len(value) + 1)
payload.append(adv_type)
payload.extend(value)
_append(0x01, struct.pack("B", (0x02 if limited_disc else 0x06) + (0x00 if br_edr else 0x04)))
if name:
_append(0x09, name)
return payload
# --- Execution ---
ble = bluetooth.BLE()
p = BLELedPeripheral(ble)
while True:
time.sleep(1)
How to Test This
Since BLE isn’t visible like a regular Wi-Fi network, you need a specialized app on your phone.
- Download nRF Connect for Mobile (Android/iOS). It’s the Swiss Army knife of Bluetooth.
- Open the app and press “Scan”.
- You should see a device named “Mi-ESP32”. Connect to it!
- You’ll see a list of “Unknown Service”. Find the one with our long UUID.
- Press the up arrow (Write) on the characteristic.
- Send the text
ON(in UTF-8 or String format) or the number1. - The LED on your board should light up!
MicroPython aioble
The example above uses the low-level API. Recently, MicroPython has released an official library called aioble that runs on top of asyncio. It is much more modern, clean, and easier to read. If you are going to do a complex project, I strongly recommend investigating aioble.
GAP and GATT in a Nutshell
BLE in MicroPython requires a mindset shift:
- GAP is for finding each other (Advertising).
- GATT is for exchanging data (Services/Characteristics).
- Everything works through Callbacks/Interrupts: you don’t “wait” for data; the data “notifies you” when it arrives.
It’s harder than Wi-Fi, but it allows your device to run for months on a simple coin cell battery.