micropython-bluetooth-ble-gatt-gap

Bluetooth Low Energy (BLE) en MicroPython

  • 6 min

El Bluetooth Low Energy (BLE) es una tecnología inalámbrica de corto alcance y bajo consumo pensada para intercambiar pequeñas cantidades de datos.

Mientras que el Wi-Fi te obliga a estar “gritando” constantemente para mantener la conexión (gastando mucha batería), el BLE está diseñado para dormir el 99% del tiempo y despertarse solo unos milisegundos para enviar un pequeño dato. Es la tecnología que mueve pulseras de actividad, sensores de puerta y etiquetas de rastreo.

Te lo digo desde el principio: programar BLE es más difícil que programar Wi-Fi. No es tan simple como “conectar y enviar”. Hay una arquitectura detrás que debemos entender.

No es el Bluetooth de tu coche

Lo primero que hay que aclarar: BLE no es Bluetooth Classic.

  • Bluetooth Classic: Diseñado para flujo de datos continuo (Audio, transferencia de archivos).
  • BLE (Low Energy): Diseñado para pequeños paquetes de datos (temperatura, estado de un botón) con un consumo ridículo.

En MicroPython, usamos el módulo estándar bluetooth.

GAP: cómo se anuncia el dispositivo

El GAP (Generic Access Profile) define cómo los dispositivos se encuentran y se conectan. Aquí hay dos roles principales:

  1. Peripheral (Periférico): Es el dispositivo “pequeño” (tu ESP32/Pico). Se anuncia diciendo “¡Estoy aquí!” y espera a que alguien se conecte.
  2. Central: Es el dispositivo “listo” (tu móvil o tablet). Escanea el ambiente buscando periféricos e inicia la conexión.

El proceso de advertising

Antes de conectarse, el Periférico envía paquetes de Advertising. Son mensajes públicos que dicen: “Me llamo ESP32-Sensor y ofrezco servicio de temperatura”.

GATT: cómo se organizan los datos

Una vez conectados, entramos en el mundo GATT (Generic Attribute Profile). Aquí es donde se intercambian los datos. GATT organiza la información como si fuera una estructura de carpetas:

  1. Perfil (Profile): Es el conjunto completo.
  2. Servicio (Service): Agrupa funcionalidades lógicas (ej: “Servicio de Batería”, “Servicio de Ritmo Cardíaco”).
  3. Característica (Characteristic): Es el dato real. Es la variable que podemos leer o escribir (ej: “Nivel de batería %”, “Latidos por minuto”).

UUID: los identificadores de Bluetooth

Cada Servicio y Característica tiene un identificador único de 128 bits llamado UUID.

  • Existen UUIDs estándar cortos (16 bits) para cosas comunes (ej: 0x180F es el Servicio de Batería).
  • Para nuestros proyectos propios, generamos UUIDs aleatorios largos.

Manos a la obra: Un periférico BLE en MicroPython

Vamos a crear un dispositivo que:

  1. Se anuncie como “Mi-ESP32”.
  2. Tenga un Servicio propio.
  3. Tenga una Característica que nos permita encender/apagar un LED escribiendo desde el móvil.

El código BLE en MicroPython es “verboso” (mucho código) porque es de bajo nivel y basado en eventos (interrupciones).

import bluetooth
import time
from machine import Pin
from micropython import const

# --- Definiciones y Constantes ---
# Eventos del sistema BLE (IRQ)
_IRQ_CENTRAL_CONNECT = const(1)
_IRQ_CENTRAL_DISCONNECT = const(2)
_IRQ_GATTS_WRITE = const(3)

# Definimos nuestros UUIDs (Generados aleatoriamente)
# Servicio: 12345678-1234-5678-1234-56789abcdef0
_MI_SERVICIO_UUID = bluetooth.UUID("12345678-1234-5678-1234-56789abcdef0")
# Característica (LED): 12345678-1234-5678-1234-56789abcdef1
_MI_CHAR_UUID = bluetooth.UUID("12345678-1234-5678-1234-56789abcdef1")

# Flags de permisos: Se puede leer (READ) y escribir (WRITE)
_FLAG_READ = const(0x0002)
_FLAG_WRITE = const(0x0008)
_FLAG_NOTIFY = const(0x0010)

# --- Clase del Periférico BLE ---
class BLELedPeripheral:
    def __init__(self, ble, name="Mi-ESP32"):
        self._ble = ble
        self._ble.active(True)
        self._ble.irq(self._irq_handler)
        
        # Configurar el LED
        self._led = Pin(2, Pin.OUT)
        self._led.value(0)

        # Registrar el servicio y la característica
        # ((UUID, FLAGS),) -> Tupla de características
        SERVICES = (
            (_MI_SERVICIO_UUID, ((_MI_CHAR_UUID, _FLAG_READ | _FLAG_WRITE),)),
        )
        
        # gatts_register_services devuelve los "handles" (manejadores)
        # Necesitamos guardarlos para saber a qué característica escribe el móvil
        ((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):
        # Esta función se ejecuta automáticamente cuando pasa algo
        
        if event == _IRQ_CENTRAL_CONNECT:
            conn_handle, _, _ = data
            print("¡Móvil conectado!", conn_handle)
            self._conn_handle = conn_handle
            
        elif event == _IRQ_CENTRAL_DISCONNECT:
            conn_handle, _, _ = data
            print("Móvil desconectado.")
            self._conn_handle = None
            # Importante: Volver a anunciarse para que otro pueda conectar
            self._advertise()
            
        elif event == _IRQ_GATTS_WRITE:
            conn_handle, value_handle = data
            # Verificamos si escribieron en nuestra característica del LED
            if conn_handle == self._conn_handle and value_handle == self._handle:
                # Leemos el valor que han escrito
                val = self._ble.gatts_read(self._handle)
                self._update_led(val)

    def _update_led(self, data):
        # Si recibimos un '1' o 'ON', encendemos
        try:
            texto = data.decode().strip()
            print("Recibido:", 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("Anunciando...")
        self._ble.gap_advertise(interval_us, adv_data=self._payload)

    # Función auxiliar para crear el paquete de anuncio (GAP)
    # Esto es técnico, básicamente empaqueta el nombre en 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

# --- Ejecución ---
ble = bluetooth.BLE()
p = BLELedPeripheral(ble)

while True:
    time.sleep(1)
Copied!

¿Cómo probamos esto?

Como el BLE no es visible como una red Wi-Fi normal, necesitas una aplicación especializada en tu móvil.

  1. Descarga nRF Connect for Mobile (Android/iOS). Es la navaja suiza del Bluetooth.
  2. Abre la app y pulsa “Scan”.
  3. Deberías ver un dispositivo llamado “Mi-ESP32”. ¡Conéctate!
  4. Verás una lista de “Unknown Service”. Busca el que tiene nuestro UUID largo.
  5. Pulsa la flecha hacia arriba (Write) en la característica.
  6. Envía el texto ON (en formato UTF-8 o String) o el número 1.
  7. ¡El LED de tu placa debería encenderse!

MicroPython aioble El ejemplo anterior usa la API de bajo nivel. Recientemente, MicroPython ha lanzado una librería oficial llamada aioble que funciona sobre asyncio. Es mucho más moderna, limpia y fácil de leer. Si vas a hacer un proyecto complejo, te recomiendo encarecidamente investigar aioble.

GAP y GATT en pocas palabras

El BLE en MicroPython requiere un cambio de mentalidad:

  • GAP es para encontrarse (Advertising).
  • GATT es para intercambiar datos (Services/Characteristics).
  • Todo funciona mediante Callbacks/Interrupciones: tú no “esperas” el dato, el dato “te avisa” cuando llega.

Es más duro que el Wi-Fi, pero permite que tu dispositivo funcione durante meses con una simple pila de botón.