ESP32 e Arduino

Gateway BLE MQTT com ESP32: Escaneie Sensores Bluetooth e Encaminhe Advertisements

Use um ESP32 como scanner Bluetooth Low Energy que coleta advertisements de sensores e beacons próximos, filtra e elimina duplicatas e os publica em topics MQTT por dispositivo.

Gateway BLE MQTT com ESP32 recebendo advertisements Bluetooth de sensores próximos e encaminhando-os via Wi-Fi
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

Um gateway BLE MQTT com ESP32 conecta os muitos sensores Bluetooth Low Energy baratos ao seu redor (termômetros, sensores de porta, beacons, monitores de plantas) a um broker MQTT via Wi-Fi. Neste tutorial você vai montar um scanner BLE passivo em um ESP32, coletar advertisements com segurança a partir da task do Bluetooth, eliminar duplicatas e publicar os dados mais recentes de cada dispositivo em seu próprio topic MQTT.

Como Funciona um Gateway BLE para MQTT

A maioria dos sensores BLE a bateria nunca aceita conexão. Eles simplesmente transmitem advertisements: pacotes curtos de até 31 bytes com um endereço, um nome opcional e campos de dados como manufacturer data ou service data. Um sensor que informa temperatura normalmente coloca a leitura em um desses campos a cada poucos segundos.

O gateway escuta esses pacotes e os encaminha. Esse é todo o truque, e ele tem duas grandes vantagens:

  • Os sensores permanecem no modo de broadcast de baixo consumo, então as baterias duram muito.
  • Um único ESP32 consegue ouvir muitos dispositivos ao mesmo tempo sem gerenciar conexões.
Abordagem Quando usar Contrapartida
Escaneamento passivo de advertisements Sensores que transmitem seus dados por broadcast Simples, escala bem, somente leitura
Escaneamento ativo (scan requests) Você precisa da scan response, muitas vezes o nome do dispositivo Um pouco mais de tráfego de rádio e consumo no sensor
Conexões GATT O dispositivo só expõe dados via characteristics, ou você precisa escrever Uma conexão por vez por slot, mais código, mais consumo

Este tutorial usa escaneamento passivo, que cobre a maioria dos casos de uso com sensores.

O Que Você Precisa

  • Uma placa ESP32 com Bluetooth (o ESP32 clássico, o ESP32-S3 e o ESP32-C3 suportam BLE; o ESP32-S2 não).
  • Arduino IDE 2.x com o pacote de placas esp32, que inclui a biblioteca BLE usada aqui.
  • A biblioteca PubSubClient.
  • Um ou mais sensores BLE, ou um app de celular capaz de transmitir um advertisement de teste.

Wi-Fi, Bluetooth e MQTT juntos geram um firmware grande. Se a compilação falhar com "Sketch too big", escolha uma partição de app maior em Tools > Partition Scheme, por exemplo "Huge APP".

Wi-Fi e BLE Compartilham o Mesmo Rádio

O ESP32 tem um único rádio de 2,4 GHz, dividido no tempo entre Wi-Fi e Bluetooth. O escalonador de coexistência cuida disso automaticamente, mas há consequências:

  • Uma janela de escaneamento de 100 por cento deixa o Wi-Fi sem tempo de antena. Deixe intervalos configurando a janela um pouco menor que o intervalo.
  • Enquanto um escaneamento bloqueante roda, o seu loop MQTT não roda. Mantenha cada escaneamento bem abaixo do intervalo de keep alive do MQTT.
  • Conte com a perda de alguns advertisements. Os sensores repetem suas transmissões, então isso normalmente não causa problema.

A Espressif documenta o comportamento de coexistência no guia de coexistência RF do ESP-IDF.

Desenho dos Topics

Topic Payload
connectmq/demo/ble-a81f/devices/<mac> {"rssi":-71,"name":"LYWSD03","mfg":"...","svc":"..."}
connectmq/demo/ble-a81f/gateway/status online / offline, retido (Last Will)
connectmq/demo/ble-a81f/gateway/stats {"seen":12,"published":9,"dropped":0}

Dois-pontos são permitidos em topics MQTT, mas o código os remove do MAC para manter os topics limpos e fáceis de digitar no terminal. Substitua ble-a81f pelo seu próprio identificador aleatório.

O Sketch do Gateway BLE MQTT com ESP32

A biblioteca BLE chama o callback de escaneamento a partir da sua própria task. Publicar dentro desse callback significaria mexer no cliente de rede a partir de duas tasks ao mesmo tempo, o que leva a travamentos aleatórios. Em vez disso, o callback copia cada advertisement para uma fila do FreeRTOS, e o loop() esvazia a fila e publica.

#include <WiFi.h>
#include <PubSubClient.h>
#include <BLEDevice.h>
#include <BLEScan.h>
#include <BLEAdvertisedDevice.h>

const char* WIFI_SSID     = "YOUR_WIFI";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
const char* MQTT_HOST     = "broker.connectmq.com";
const uint16_t MQTT_PORT  = 1883;
const char* CLIENT_ID     = "ble-a81f-gateway";
const char* PREFIX        = "connectmq/demo/ble-a81f";

const int   SCAN_SECONDS     = 5;
const int   MIN_RSSI         = -90;    // ignora dispositivos com sinal muito fraco
const uint32_t MIN_REPUBLISH_MS = 10000; // limite de taxa por dispositivo

struct Adv {
  char addr[18];
  char name[24];
  int  rssi;
  uint8_t mfg[31];
  uint8_t mfgLen;
};

QueueHandle_t advQueue;
WiFiClient net;
PubSubClient mqtt(net);
BLEScan* scanner;
uint32_t seen = 0, published = 0, dropped = 0;

// Memória simples por dispositivo para limitar a taxa
struct Recent { char addr[18]; uint32_t lastMs; };
Recent recent[32];

class ScanCallbacks : public BLEAdvertisedDeviceCallbacks {
  void onResult(BLEAdvertisedDevice dev) override {
    if (dev.getRSSI() < MIN_RSSI) return;
    Adv a = {};
    auto addr = dev.getAddress().toString();
    strncpy(a.addr, addr.c_str(), sizeof(a.addr) - 1);
    if (dev.haveName()) {
      auto n = dev.getName();
      strncpy(a.name, n.c_str(), sizeof(a.name) - 1);
    }
    a.rssi = dev.getRSSI();
    if (dev.haveManufacturerData()) {
      auto md = dev.getManufacturerData();
      a.mfgLen = min((size_t)md.length(), sizeof(a.mfg));
      memcpy(a.mfg, md.c_str(), a.mfgLen);
    }
    seen++;
    if (xQueueSend(advQueue, &a, 0) != pdTRUE) dropped++;
  }
};

bool shouldPublish(const char* addr) {
  uint32_t now = millis();
  int freeSlot = -1, oldest = 0;
  for (int i = 0; i < 32; i++) {
    if (strcmp(recent[i].addr, addr) == 0) {
      if (now - recent[i].lastMs < MIN_REPUBLISH_MS) return false;
      recent[i].lastMs = now;
      return true;
    }
    if (recent[i].addr[0] == 0 && freeSlot < 0) freeSlot = i;
    if (recent[i].lastMs < recent[oldest].lastMs) oldest = i;
  }
  int slot = freeSlot >= 0 ? freeSlot : oldest;
  strncpy(recent[slot].addr, addr, sizeof(recent[slot].addr) - 1);
  recent[slot].lastMs = now;
  return true;
}

void publishAdv(const Adv& a) {
  char mac[13]; int j = 0;
  for (int i = 0; a.addr[i] && j < 12; i++) if (a.addr[i] != ':') mac[j++] = a.addr[i];
  mac[j] = 0;

  char hex[63] = {0};
  for (int i = 0; i < a.mfgLen; i++) sprintf(hex + i * 2, "%02x", a.mfg[i]);

  char topic[96], payload[200];
  snprintf(topic, sizeof(topic), "%s/devices/%s", PREFIX, mac);
  snprintf(payload, sizeof(payload), "{\"rssi\":%d,\"name\":\"%s\",\"mfg\":\"%s\"}",
           a.rssi, a.name, hex);
  if (mqtt.publish(topic, payload)) published++;
}

void connectWifi() {
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  while (WiFi.status() != WL_CONNECTED) delay(500);
}

void connectMqtt() {
  char st[96];
  snprintf(st, sizeof(st), "%s/gateway/status", PREFIX);
  while (!mqtt.connected()) {
    if (mqtt.connect(CLIENT_ID, st, 1, true, "offline")) {
      mqtt.publish(st, "online", true);
    } else {
      delay(5000);
    }
  }
}

void setup() {
  Serial.begin(115200);
  advQueue = xQueueCreate(40, sizeof(Adv));

  connectWifi();
  mqtt.setServer(MQTT_HOST, MQTT_PORT);
  mqtt.setBufferSize(512);
  mqtt.setKeepAlive(30);
  connectMqtt();

  BLEDevice::init("");
  scanner = BLEDevice::getScan();
  scanner->setAdvertisedDeviceCallbacks(new ScanCallbacks(), true); // reporta duplicatas
  scanner->setActiveScan(false);   // passivo: não envia scan requests
  scanner->setInterval(160);       // unidades de 0,625 ms
  scanner->setWindow(80);          // deixa tempo de antena para o Wi-Fi
}

void loop() {
  if (WiFi.status() != WL_CONNECTED) connectWifi();
  if (!mqtt.connected()) connectMqtt();

  scanner->start(SCAN_SECONDS, false);   // escaneamento bloqueante, resultados vão para a fila
  scanner->clearResults();               // libera a memória ocupada pelo escaneamento

  Adv a;
  while (xQueueReceive(advQueue, &a, 0) == pdTRUE) {
    if (shouldPublish(a.addr)) publishAdv(a);
    mqtt.loop();
  }

  char topic[96], stats[96];
  snprintf(topic, sizeof(topic), "%s/gateway/stats", PREFIX);
  snprintf(stats, sizeof(stats), "{\"seen\":%lu,\"published\":%lu,\"dropped\":%lu}",
           (unsigned long)seen, (unsigned long)published, (unsigned long)dropped);
  mqtt.publish(topic, stats);
  mqtt.loop();
}

Acompanhe o tráfego pelo seu computador:

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

Em poucos segundos deve aparecer uma linha por dispositivo próximo. Em uma casa, um escritório ou um galpão típicos, celulares, fones, TVs e relógios fazem advertising, então espere ver mais dispositivos do que os sensores que você tem.

Filtrando o Que Você Encaminha

Encaminhar todo advertisement ao alcance é útil para descoberta, mas gera ruído em produção e pode esbarrar em limites de taxa de brokers compartilhados (o servidor MQTT público em broker.connectmq.com permite 10 mensagens por segundo por cliente). Três filtros mantêm o tráfego sob controle:

  1. Limite de RSSI. MIN_RSSI descarta dispositivos distantes ou atrás de paredes.
  2. Limite de taxa por dispositivo. MIN_REPUBLISH_MS publica cada dispositivo no máximo uma vez a cada dez segundos, mesmo que ele faça advertising a cada segundo.
  3. Lista de permissão. Quando você souber os endereços ou os IDs de fabricante dos seus sensores, aceite apenas esses. Os dois primeiros bytes do manufacturer data são o identificador da empresa, o que é um bom filtro para uma marca específica.

Observe que muitos celulares e alguns acessórios usam endereços aleatórios e rotativos por privacidade, então o mesmo celular pode aparecer com vários MACs. Sensores normalmente usam um endereço fixo.

Decodificando os Dados dos Sensores

O gateway encaminha o manufacturer data em hexadecimal e deixa a decodificação para um assinante. Isso é proposital: atualizar firmware é difícil, enquanto um assinante em Python ou Node-RED aprende novos formatos de sensor em minutos. Muitos firmwares de sensor open source seguem formatos documentados, como o service data do BTHome, que um assinante pode decodificar em temperatura, umidade e bateria e republicar em um topic limpo como home/livingroom/climate.

Essa divisão (gateway simples e confiável na borda; lógica flexível em um assinante) é o mesmo padrão usado por gateways industriais maiores, descrito no artigo sobre gateway de borda IIoT.

Checklist de Diagnóstico para o Gateway BLE MQTT com ESP32

  1. Nenhum dispositivo encontrado. Confirme que a sua placa suporta BLE (o ESP32-S2 não suporta) e que o sensor está fazendo advertising, usando um app de scanner no celular.
  2. O MQTT desconecta durante os escaneamentos. Reduza SCAN_SECONDS ou aumente o keep alive; o cliente não consegue responder pings enquanto o escaneamento bloqueia.
  3. Resets aleatórios. Verifique se você não está publicando a partir do callback de escaneamento e se a fila é grande o suficiente. Um contador dropped crescente indica que ela é pequena demais.
  4. Wi-Fi lento ou caindo. Reduza a janela de escaneamento em relação ao intervalo para o Wi-Fi ter mais tempo de antena.
  5. Heap diminuindo com o tempo. Chame clearResults() após cada escaneamento, como o sketch faz.

Para estratégias de reconexão que sobrevivem a reinicializações do roteador, veja reconexão MQTT no ESP32 e, para mais projetos com sensores, o tutorial de IoT com ESP32.

Protegendo o Gateway

Advertisements BLE são públicos por natureza: qualquer pessoa por perto pode lê-los. Não piore a situação publicando-os em um topic público em produção. Migre para um broker privado na porta TLS 8883 com WiFiClientSecure e credenciais do portal do seu broker. Um broker MQTT gerenciado como a ConnectMQ dá a cada gateway suas próprias credenciais sem que você precise manter servidores e, enquanto você prototipa, o servidor MQTT público (gratuito) serve bem para testes e protótipos, desde que não haja nada sensível ao alcance, como dados pessoais protegidos pela LGPD.

Comece a Usar a ConnectMQ

Quando o gateway estiver pronto para uso real, dê a ele um endpoint privado e criptografado na ConnectMQ. O explorador de topics ao vivo do portal mostra cada topic de dispositivo assim que ele aparece, a forma mais rápida de ver o que o seu gateway está ouvindo. Comece no plano Serverless gratuito em app.connectmq.com, sem cartão de crédito.

Perguntas frequentes

Quantos sensores BLE um gateway ESP32 consegue atender?

Com escaneamento passivo, dezenas de dispositivos ao alcance é normal; os limites práticos são o tempo de antena, o tamanho da fila e a frequência de publicação. Limite a taxa por dispositivo e filtre os sensores que interessam.

Devo usar NimBLE em vez da biblioteca BLE integrada?

A NimBLE-Arduino usa menos memória e é uma boa escolha para gateways maiores. A API dela é um pouco diferente, então adapte o código do callback se trocar.

O gateway consegue se conectar a sensores que exigem pareamento?

Sim, mas isso exige conexões GATT em vez de escaneamento, e apenas algumas conexões por vez. Para sensores de broadcast, o escaneamento passivo é mais simples e confiável.

Por que vejo dispositivos que não são meus?

Celulares, TVs, fones de ouvido e muitos outros produtos fazem advertising o tempo todo. Use um limite de RSSI e uma lista de permissão de endereços ou IDs de fabricante.

Artigos relacionados

Exemplo de MQTT com ESP-IDF: Monte um Cliente esp-mqtt do Zero

Monte um projeto ESP-IDF a partir de uma pasta vazia com o componente esp-mqtt: configuração do Wi-Fi em modo station, o handler de eventos MQTT, publicação e inscrição, propriedades do MQTT 5 e TLS com o bundle de certificados.

8 min de leitura

Usando os Recursos do MQTT 5 do esp-mqtt no ESP32 com ESP-IDF

Um projeto ESP-IDF prático que ativa o MQTT 5.0 no esp-mqtt e usa user properties, expiração de mensagens, topic aliases, expiração de sessão e response topics com um broker MQTT 5 real.

8 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.