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
- No se encuentran dispositivos: ejecuta
bluetoothctl power onyrfkill 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. - Se encuentran dispositivos pero sin service data: el sensor usa el firmware de fábrica, que cifra sus anuncios, o utiliza otro formato. Imprime
adven el callback para inspeccionar los campos en bruto. org.bluez.Error.InProgress: otro proceso ya está escaneando. Detén los demás escáneres o ejecuta un solo puente por adaptador.- 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=alwayslo resuelve, como se describe en ejecutar un script MQTT de Python al arrancar. - 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.



