Raspberry Pi

Puente BLE a MQTT con Raspberry Pi, Python y Bleak

Usa el Bluetooth integrado de la Raspberry Pi, la biblioteca bleak y paho-mqtt para escanear los anuncios de sensores BLE, decodificar temperatura y humedad y publicar las lecturas por MQTT con una frecuencia controlada.

Raspberry Pi recibiendo señales Bluetooth de pequeños sensores, que ilustra un puente BLE a MQTT con Raspberry Pi
Pruébalo en tu propio broker MQTTConsigue un broker MQTT privado y gestionado con el plan Free y empieza con ejemplos de código listos para usar.
Pruébalo gratis Ejemplos de código Sin tarjeta

Un puente BLE a MQTT con Raspberry Pi convierte sensores Bluetooth baratos y a batería en dispositivos conectados que puedes graficar, vigilar con alertas y automatizar. En este tutorial usarás la radio Bluetooth integrada de la Pi, la biblioteca bleak y paho-mqtt para escuchar los anuncios de los sensores, decodificar los valores de temperatura, humedad y batería, y publicarlos en un broker MQTT con una frecuencia razonable. Primero probarás contra broker.connectmq.com y después pasarás a credenciales privadas.

Por qué hacer de puente entre BLE y MQTT

Los sensores Bluetooth Low Energy son pequeños, económicos y funcionan un año o más con una pila de botón, pero su alcance es de una o dos habitaciones y no pueden llegar a internet por sí solos. Una Raspberry Pi con radio integrada (Pi 3B y posteriores, Zero W y Zero 2 W) puede situarse en el centro de una casa, un invernadero o una cámara frigorífica, oír todos los sensores cercanos y reenviar los datos por Wi-Fi o Ethernet.

La mayoría de los sensores emiten sus lecturas en paquetes de anuncio (advertisements). Eso resulta cómodo: el puente nunca necesita conectarse al sensor, así que se pueden leer docenas de dispositivos de forma pasiva sin agotar sus baterías. La lectura basada en conexión (GATT) sigue siendo posible con bleak, pero es más lenta, consume más energía en el sensor y limita cuántos dispositivos puedes manejar a la vez. Esta guía se centra en los anuncios, con una nota sobre GATT al final.

Qué necesitas

Elemento Notas
Raspberry Pi con Bluetooth Pi 3B, 4, 5, Zero W o Zero 2 W; también sirve un dongle BLE USB
Raspberry Pi OS Bookworm BlueZ viene instalado por defecto
Sensores BLE Este ejemplo decodifica termómetros Xiaomi LYWSD03MMC con el popular firmware personalizado de código abierto
Python 3.11+ Incluido en Bookworm

La lógica de decodificación depende del dispositivo, pero la estructura del puente no. Si tienes otro sensor, solo cambias una función.

Instalar bleak y paho-mqtt

bleak usa BlueZ a través de D-Bus en Linux, así que en una instalación estándar no necesitas acceso root ni paquetes de sistema adicionales. Comprueba que la radio está activa y crea un entorno virtual:

bluetoothctl show | grep -E "Powered|Name"
sudo rfkill unblock bluetooth
python3 -m venv ~/ble-bridge/.venv
source ~/ble-bridge/.venv/bin/activate
pip install bleak "paho-mqtt>=2.0"

Un escaneo rápido confirma que bleak ve los dispositivos:

python -c "import asyncio; from bleak import BleakScanner; print(*asyncio.run(BleakScanner.discover(timeout=8)), sep='\n')"

Decodificar los anuncios

El firmware personalizado de los sensores LYWSD03MMC emite las lecturas como service data bajo el UUID de Environmental Sensing 0x181A. Hay dos formatos habituales, y se distinguen por la longitud:

  • Formato pvvx, 15 bytes, little-endian: MAC (6 bytes, invertida), temperatura como int16 en 0,01 grados C, humedad como uint16 en 0,01 por ciento, milivoltios de batería como uint16, porcentaje de batería, un contador de tramas y flags.
  • Formato ATC1441, 13 bytes, big-endian: MAC (6 bytes), temperatura como int16 en 0,1 grados C, porcentaje de humedad, porcentaje de batería, milivoltios de batería como uint16 y un contador.

Decodificarlos con struct es breve y fácil de probar. Para otros dispositivos, examina el diccionario manufacturer_data (indexado por ID de fabricante) o service_data (indexado por la cadena del UUID) del anuncio y aplica el formato documentado por el fabricante.

El script del puente BLE a MQTT con Raspberry Pi

El script ejecuta un escáner continuo con un callback de detección. Cada anuncio de un sensor conocido se decodifica y se guarda; una tarea asíncrona aparte publica la última lectura de cada sensor como máximo una vez cada 30 segundos. paho-mqtt ejecuta su propio hilo de red mediante loop_start(), así que convive sin problemas con asyncio.

import asyncio
import json
import os
import struct
import time
import uuid

import paho.mqtt.client as mqtt
from bleak import BleakScanner

BROKER = os.getenv("MQTT_HOST", "broker.connectmq.com")
PORT = int(os.getenv("MQTT_PORT", "1883"))
USERNAME = os.getenv("MQTT_USERNAME")
PASSWORD = os.getenv("MQTT_PASSWORD")
CLIENT_ID = os.getenv("MQTT_CLIENT_ID", f"ble-bridge-{uuid.uuid4().hex[:8]}")
PREFIX = os.getenv("TOPIC_PREFIX", "connectmq/demo/b4n8r2/ble")
PUBLISH_EVERY = 30  # segundos por sensor

ESS_UUID = "0000181a-0000-1000-8000-00805f9b34fb"
latest = {}        # mac -> lectura
last_sent = {}     # mac -> marca de tiempo


def decode_181a(data: bytes):
    if len(data) == 15:  # formato personalizado pvvx
        temp, hum, mv, batt = struct.unpack_from("<hHHB", data, 6)
        return {"temperature": temp / 100, "humidity": hum / 100,
                "battery_mv": mv, "battery": batt}
    if len(data) == 13:  # formato ATC1441
        temp, hum, batt, mv = struct.unpack_from(">hBBH", data, 6)
        return {"temperature": temp / 10, "humidity": hum,
                "battery_mv": mv, "battery": batt}
    return None


def on_advertisement(device, adv):
    data = adv.service_data.get(ESS_UUID)
    if not data:
        return
    reading = decode_181a(data)
    if reading:
        reading["rssi"] = adv.rssi
        reading["ts"] = int(time.time())
        latest[device.address.replace(":", "").lower()] = reading


def on_connect(client, userdata, flags, reason_code, properties):
    print("MQTT connected:", reason_code)
    client.publish(f"{PREFIX}/bridge/status", "online", qos=1, retain=True)


def make_client():
    c = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=CLIENT_ID)
    if USERNAME:
        c.username_pw_set(USERNAME, PASSWORD)
    if PORT == 8883:
        c.tls_set()
    c.will_set(f"{PREFIX}/bridge/status", "offline", qos=1, retain=True)
    c.reconnect_delay_set(1, 60)
    c.on_connect = on_connect
    c.connect_async(BROKER, PORT, keepalive=60)
    c.loop_start()
    return c


async def publisher(client):
    while True:
        now = time.time()
        for mac, reading in list(latest.items()):
            if now - last_sent.get(mac, 0) >= PUBLISH_EVERY and client.is_connected():
                client.publish(f"{PREFIX}/{mac}", json.dumps(reading), qos=1, retain=True)
                last_sent[mac] = now
        await asyncio.sleep(1)


async def main():
    client = make_client()
    scanner = BleakScanner(detection_callback=on_advertisement)
    await scanner.start()
    print("scanning...")
    try:
        await publisher(client)
    finally:
        await scanner.stop()
        client.publish(f"{PREFIX}/bridge/status", "offline", qos=1, retain=True)
        client.loop_stop()
        client.disconnect()


if __name__ == "__main__":
    asyncio.run(main())

Suscríbete desde tu portátil para ver llegar las lecturas:

mosquitto_sub -h broker.connectmq.com -p 1883 -i ble-viewer-b4n8r2 -t "connectmq/demo/b4n8r2/ble/#" -v

Cada sensor tiene su propio topic con el nombre de su dirección MAC, y la lectura es retenida, de modo que un dashboard que se conecte más tarde ve de inmediato el valor más reciente. Si prefieres nombres legibles, mantén un pequeño diccionario que asigne direcciones MAC a nombres de habitaciones y úsalos en el topic.

Decisiones de diseño que importan

Limita la frecuencia por dispositivo. Los sensores pueden emitir anuncios cada pocos segundos. Publicar cada uno desperdicia ancho de banda y, en el servidor MQTT público gratuito, alcanza rápidamente el límite de 10 mensajes por segundo por cliente cuando tienes muchos sensores. Una ventana de 30 segundos por sensor es más que suficiente para el clima de una habitación.

Retén el último estado, no el historial. Los mensajes retenidos dan a los nuevos suscriptores el valor actual al instante. El historial pertenece a una base de datos: un suscriptor que escriba en SQLite o InfluxDB, como el de la guía de Grafana e InfluxDB en Raspberry Pi, es el lugar adecuado.

Incluye el RSSI. La intensidad de señal te avisa cuando un sensor está a punto de quedar fuera de alcance o cuando hay que mover el puente. También es una forma barata de detectar un sensor que está fallando antes de que baje su lectura de batería.

Vigila el silencio. Un sensor que deja de emitir anuncios es invisible para el puente. Añade el campo ts a los dashboards o alerta cuando envejezca. Las reglas de alerta por umbral de ConnectMQ funcionan con valores numéricos, así que un suscriptor que publique los "segundos desde la última lectura" de cada sensor te da una alarma de datos obsoletos sencilla.

Solución de problemas

  1. No se encuentran dispositivos: ejecuta bluetoothctl power on y rfkill list. En una Pi donde Wi-Fi y Bluetooth comparten chip, un tráfico Wi-Fi intenso en 2,4 GHz puede reducir la recepción BLE; Ethernet o Wi-Fi de 5 GHz ayudan.
  2. Se encuentran dispositivos pero sin service data: el sensor usa el firmware de fábrica, que cifra sus anuncios, o utiliza otro formato. Imprime adv en el callback para inspeccionar los campos en bruto.
  3. org.bluez.Error.InProgress: otro proceso ya está escaneando. Detén los demás escáneres o ejecuta un solo puente por adaptador.
  4. Las lecturas se detienen al cabo de horas: BlueZ a veces necesita un reinicio en kernels antiguos. Ejecutar el puente como servicio systemd con Restart=always lo resuelve, como se describe en ejecutar un script MQTT de Python al arrancar.
  5. Alcance débil: un cable alargador USB para alejar el dongle de la carcasa metálica de la Pi y de los puertos USB 3 puede marcar una diferencia notable, ya que se sabe que USB 3 provoca interferencias en 2,4 GHz.

Leer características GATT

Algunos dispositivos solo exponen sus datos mediante una conexión. bleak lo gestiona con BleakClient:

from bleak import BleakClient

async def read_battery(address):
    async with BleakClient(address) as client:
        value = await client.read_gatt_char("00002a19-0000-1000-8000-00805f9b34fb")
        return value[0]

Las conexiones son costosas, así que lee los valores GATT con un calendario lento y detén el escaneo mientras te conectas si tu adaptador tiene problemas para hacer ambas cosas a la vez. La documentación de bleak explica las notificaciones, que son la forma eficiente de transmitir datos con conexión.

Pasar a producción

Cuando el puente funcione, abandona el servidor público, que es legible por cualquiera. Crea un broker privado en ConnectMQ, define MQTT_HOST, MQTT_PORT=8883, MQTT_USERNAME, MQTT_PASSWORD y MQTT_CLIENT_ID a partir del portal, y cambia el prefijo de topics por algo como home/ble. El puente no necesita puertos de entrada, solo una conexión TLS de salida, así que funciona detrás de cualquier router doméstico o módem celular. Un broker MQTT gestionado también significa que no hay ningún proceso de broker que mantener en la Pi.

Si MQTT en la Pi es nuevo para ti, el tutorial de MQTT con Raspberry Pi cubre lo básico, y el artículo sobre el gateway IoT con Raspberry Pi muestra cómo combinar BLE con otros protocolos en un solo dispositivo.

Conecta tus sensores BLE a ConnectMQ

Regístrate en app.connectmq.com en el plan Serverless gratuito, sin tarjeta de crédito, y tendrás credenciales de broker en un par de minutos. Apunta el puente hacia ellas y abre el explorador de topics en vivo para ver aparecer cada sensor bajo su propio topic.

Preguntas frecuentes

¿Cuántos sensores BLE puede manejar una Raspberry Pi?

Con escaneo pasivo de anuncios, lo habitual son docenas de sensores dentro del alcance de radio, porque la Pi solo escucha. El alcance y las paredes suelen limitarte antes que la CPU.

¿Tengo que emparejar los sensores?

No. Los sensores basados en anuncios emiten para cualquiera que escuche, así que no hace falta emparejarlos ni conectarse a ellos. Los anuncios cifrados son la excepción y necesitan la clave del dispositivo.

¿Puedo ejecutar esto en una Raspberry Pi Zero 2 W?

Sí. El puente usa poca CPU y memoria, y la Zero 2 W tiene Bluetooth y Wi-Fi integrados, lo que la convierte en un buen puente de bajo coste por habitación o edificio.

¿Debo usar QoS 0 o QoS 1 para las lecturas de sensores?

QoS 1 es un buen valor por defecto para lecturas que quieres que se entreguen al menos una vez. Para valores muy frecuentes en los que la siguiente lectura sustituye a la anterior, QoS 0 basta.

Artículos relacionados

Fiabilidad de la Raspberry Pi: cómo usar una Pi 24/7 como gateway IoT

Una guía práctica para mantener un gateway MQTT con Raspberry Pi funcionando durante meses: desgaste del almacenamiento, sistemas de archivos de solo lectura, alimentación, refrigeración, watchdogs de hardware y software y monitorización remota de la salud.

9 min de lectura

Pico W vs Pico 2 W para MQTT: rendimiento, memoria y TLS de RP2040 frente a RP2350

La Pico 2 W mantiene el formato y el chip Wi-Fi de la Pico W, pero sustituye el RP2040 por el RP2350, más rápido. Esta comparativa analiza qué significa para los dispositivos MQTT: CPU, memoria, TLS, funciones de seguridad, soporte de software y qué placa comprar para cada proyecto.

9 min de lectura

Registro de accesos Raspberry Pi RFID MQTT con el lector RC522

Conecta un lector RFID RC522 a una Raspberry Pi, lee etiquetas en Python, compáralas con una lista blanca local y publica cada intento de acceso como un evento MQTT que pueden consumir registradores, dashboards y herramientas de alertas.

8 min de lectura

Todos los artículos →

Tu broker MQTT privado, a unos pocos clics

Crea una cuenta, obtén las credenciales de tu broker y conecta tu primer dispositivo, sin servidores que instalar, parchear ni escalar.