Raspberry Pi

Ponte BLE para MQTT com Raspberry Pi usando Python e Bleak

Use o Bluetooth integrado do Raspberry Pi, a biblioteca bleak e o paho-mqtt para escanear advertisements de sensores BLE, decodificar temperatura e umidade e publicar leituras limitadas em MQTT.

Raspberry Pi recebendo sinais Bluetooth de pequenos sensores, ilustrando uma ponte Raspberry Pi BLE para MQTT
Rode isso no seu próprio broker MQTTTenha um broker MQTT privado e gerenciado no plano Free e comece com exemplos de código prontos.
Teste grátis Exemplos de código Sem cartão de crédito

Uma ponte Raspberry Pi BLE para MQTT transforma sensores Bluetooth baratos e alimentados por bateria em dispositivos de rede que você pode colocar em gráficos, monitorar com alertas e automatizar. Neste tutorial você vai usar o rádio Bluetooth integrado do Pi, a biblioteca bleak e o paho-mqtt para escutar os advertisements dos sensores, decodificar temperatura, umidade e bateria e publicá-los em um broker MQTT em uma taxa sensata. Primeiro você testa contra o broker.connectmq.com, o servidor MQTT público do ConnectMQ, e depois migra para credenciais privadas.

Por que fazer a ponte de BLE para MQTT

Sensores Bluetooth Low Energy são pequenos, baratos e funcionam por um ano ou mais com uma bateria tipo moeda, mas seu alcance é de um ou dois cômodos e eles não chegam à internet sozinhos. Um Raspberry Pi com rádio integrado (Pi 3B e posteriores, Zero W e Zero 2 W) pode ficar no meio de uma casa, estufa ou câmara fria, ouvir todos os sensores próximos e encaminhar os dados via Wi-Fi ou Ethernet.

A maioria dos sensores transmite suas leituras em pacotes de advertisement. Isso é conveniente: a ponte nunca precisa se conectar ao sensor, então dezenas de dispositivos podem ser lidos passivamente sem esgotar suas baterias. A leitura baseada em conexão (GATT) continua possível com o bleak, mas é mais lenta, consome mais energia do sensor e limita quantos dispositivos você consegue tratar ao mesmo tempo. Este guia foca nos advertisements, com uma observação sobre GATT no final.

O que você precisa

Item Observações
Raspberry Pi com Bluetooth Pi 3B, 4, 5, Zero W ou Zero 2 W; um dongle BLE USB também funciona
Raspberry Pi OS Bookworm O BlueZ vem instalado por padrão
Sensores BLE Este exemplo decodifica termômetros Xiaomi LYWSD03MMC com o popular firmware customizado de código aberto
Python 3.11+ Vem com o Bookworm

A lógica de decodificação é específica de cada dispositivo, mas a estrutura da ponte não. Se você tem um sensor diferente, só precisa trocar uma função.

Instale o bleak e o paho-mqtt

O bleak usa o BlueZ via D-Bus no Linux, então não é preciso acesso root nem pacotes extras de sistema em uma instalação padrão. Verifique se o rádio está ativo e crie um ambiente 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"

Um scan rápido confirma que o bleak enxerga os dispositivos:

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

Decodificando advertisements

O firmware customizado dos sensores LYWSD03MMC transmite as leituras como service data sob o UUID de Environmental Sensing 0x181A. Dois layouts são comuns, e dá para diferenciá-los pelo tamanho:

  • Formato pvvx, 15 bytes, little-endian: MAC (6 bytes, invertido), temperatura como int16 em 0,01 grau C, umidade como uint16 em 0,01 por cento, milivolts da bateria como uint16, porcentagem da bateria, um contador de frames e flags.
  • Formato ATC1441, 13 bytes, big-endian: MAC (6 bytes), temperatura como int16 em 0,1 grau C, porcentagem de umidade, porcentagem da bateria, milivolts da bateria como uint16 e um contador.

Decodificar isso com struct é curto e testável. Para outros dispositivos, olhe o dicionário manufacturer_data (indexado pelo ID da empresa) ou service_data (indexado pela string do UUID) no advertisement e aplique o formato documentado pelo fabricante.

O script da ponte Raspberry Pi BLE para MQTT

O script roda um scanner contínuo com um callback de detecção. Cada advertisement de um sensor conhecido é decodificado e armazenado; uma tarefa async separada publica a leitura mais recente de cada sensor no máximo uma vez a cada 30 segundos. O paho-mqtt roda sua própria thread de rede via loop_start(), então convive bem com o 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 -> leitura
last_sent = {}     # mac -> timestamp


def decode_181a(data: bytes):
    if len(data) == 15:  # formato customizado 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())

Assine a partir do seu notebook para ver as leituras chegando:

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

Cada sensor ganha seu próprio topic com o nome do seu endereço MAC, e a leitura é retida, então um dashboard que conecta depois vê imediatamente o valor mais recente. Se preferir nomes amigáveis, mantenha um pequeno dicionário que mapeia endereços MAC para nomes de cômodos e use-os no topic.

Decisões de projeto que importam

Limite a taxa por dispositivo. Os sensores podem enviar advertisements a cada poucos segundos. Publicar cada um desperdiça banda e, no servidor MQTT público gratuito, atinge rapidamente o limite de 10 mensagens por segundo por cliente quando você tem muitos sensores. Uma janela de 30 segundos por sensor é mais que suficiente para o clima de um ambiente.

Retenha o estado mais recente, não o histórico. Retained messages entregam o valor atual instantaneamente a novos subscribers. O histórico pertence a um banco de dados: um subscriber gravando em SQLite ou InfluxDB, como o do guia de Grafana e InfluxDB no Raspberry Pi, é o lugar certo para ele.

Inclua o RSSI. A intensidade do sinal mostra quando um sensor está prestes a sair do alcance ou quando a ponte precisa mudar de lugar. Também é uma forma barata de detectar um sensor morrendo antes que a leitura da bateria caia.

Fique atento ao silêncio. Um sensor que para de enviar advertisements fica invisível para a ponte. Adicione o campo ts aos dashboards, ou alerte quando ele ficar antigo. As regras de alerta por limite do ConnectMQ funcionam com valores numéricos, então um subscriber que publica "segundos desde a última leitura" por sensor dá a você um alarme simples de dados desatualizados.

Solução de problemas

  1. Nenhum dispositivo encontrado: rode bluetoothctl power on e rfkill list. Em um Pi em que Wi-Fi e Bluetooth compartilham um único chip, tráfego Wi-Fi intenso em 2,4 GHz pode reduzir a recepção BLE; Ethernet ou Wi-Fi 5 GHz ajudam.
  2. Dispositivos encontrados, mas sem service data: o sensor está com o firmware de fábrica, que criptografa seus advertisements, ou usa outro formato. Imprima adv no callback para inspecionar os campos brutos.
  3. org.bluez.Error.InProgress: outro processo já está escaneando. Pare os outros scanners ou rode apenas uma ponte por adaptador.
  4. As leituras param após horas: o BlueZ às vezes precisa ser reiniciado em kernels mais antigos. Rodar a ponte como serviço do systemd com Restart=always resolve isso, como descrito em como rodar um script Python MQTT no boot.
  5. Alcance fraco: um cabo extensor USB para afastar o dongle da caixa metálica do Pi e das portas USB 3 pode fazer uma diferença perceptível, já que o USB 3 é conhecido por causar interferência em 2,4 GHz.

Lendo características GATT

Alguns dispositivos expõem dados apenas por meio de uma conexão. O bleak resolve isso com 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]

Conexões são caras, então leia valores GATT em um cronograma lento e pare de escanear enquanto conecta se o seu adaptador tiver dificuldade em fazer as duas coisas ao mesmo tempo. A documentação do bleak cobre as notificações, que são a forma eficiente de transmitir dados com conexão.

Indo para produção

Quando a ponte estiver funcionando, saia do servidor público, que pode ser lido por qualquer pessoa. Crie um broker privado no ConnectMQ, defina MQTT_HOST, MQTT_PORT=8883, MQTT_USERNAME, MQTT_PASSWORD e MQTT_CLIENT_ID a partir do portal e troque o prefixo de topic por algo como home/ble. A ponte não precisa de portas de entrada, apenas de uma conexão TLS de saída, então funciona atrás de qualquer roteador doméstico ou modem celular. Um broker MQTT gerenciado também significa que não há processo de broker no Pi para manter.

Se MQTT no Pi for novidade para você, o tutorial de MQTT com Raspberry Pi cobre o básico, e o artigo sobre gateway IoT com Raspberry Pi mostra como combinar BLE com outros protocolos em um único dispositivo.

Conecte seus sensores BLE ao ConnectMQ

Cadastre-se em app.connectmq.com no plano Serverless gratuito, sem cartão de crédito, e você terá credenciais de broker em poucos minutos. Aponte a ponte para elas e abra o explorador de topics ao vivo para ver cada sensor aparecer em seu próprio topic.

Perguntas frequentes

Quantos sensores BLE um Raspberry Pi consegue tratar?

Com scan passivo de advertisements, dezenas de sensores dentro do alcance do rádio são comuns, porque o Pi apenas escuta. O alcance e as paredes costumam limitar antes da CPU.

Preciso parear os sensores?

Não. Sensores baseados em advertisement transmitem para quem estiver ouvindo, então não é preciso pareamento nem conexão. A exceção são os advertisements criptografados, que exigem a chave do dispositivo.

Posso rodar isso em um Raspberry Pi Zero 2 W?

Sim. A ponte usa pouca CPU e memória, e o Zero 2 W tem Bluetooth e Wi-Fi integrados, o que o torna uma boa ponte de baixo custo por cômodo ou prédio.

Devo usar QoS 0 ou QoS 1 para leituras de sensores?

QoS 1 é um bom padrão para leituras que você quer entregues pelo menos uma vez. Para valores muito frequentes, em que a próxima leitura substitui a anterior, QoS 0 serve bem.

Artigos relacionados

Registro de Acessos Raspberry Pi RFID MQTT com o Leitor RC522

Ligue um leitor RFID RC522 a um Raspberry Pi, leia tags em Python, confira-as em uma lista de permissões local e publique cada tentativa de acesso como um evento MQTT que loggers, dashboards e ferramentas de alerta podem consumir.

8 min de leitura

Confiabilidade do Raspberry Pi: Rodando um Pi 24/7 como Gateway IoT

Um guia prático para manter um gateway MQTT com Raspberry Pi rodando por meses: desgaste do armazenamento, sistemas de arquivos somente leitura, alimentação, refrigeração, watchdogs de hardware e software e monitoramento remoto de saúde.

9 min de leitura

Pico W vs Pico 2 W para MQTT: Desempenho, Memória e TLS do RP2040 vs RP2350

O Pico 2 W mantém o formato e o chip Wi-Fi do Pico W, mas troca o RP2040 pelo RP2350, mais rápido. Esta comparação mostra o que isso significa para dispositivos MQTT: CPU, memória, TLS, recursos de segurança, suporte de software e qual placa comprar para cada projeto.

9 min de leitura

Todos os artigos →

Seu broker MQTT privado está a poucos cliques de distância

Crie uma conta, receba as credenciais do seu broker e conecte seu primeiro dispositivo — sem servidores para instalar, atualizar ou escalar.