ESP32 und Arduino

ESP32 BLE MQTT Gateway: Bluetooth-Sensoren scannen und Advertisements weiterleiten

Nutzen Sie einen ESP32 als Bluetooth-Low-Energy-Scanner, der Advertisements von Sensoren und Beacons in der Nähe sammelt, filtert, Duplikate entfernt und sie auf geräteeigenen MQTT-Topics veröffentlicht.

ESP32 BLE MQTT Gateway, das Bluetooth-Advertisements von Sensoren in der Nähe empfängt und über WLAN weiterleitet
Probieren Sie es auf Ihrem eigenen MQTT-Broker ausHolen Sie sich im Free-Tarif einen privaten, verwalteten MQTT-Broker und starten Sie mit fertigen Codebeispielen.
Kostenlos testen Codebeispiele Keine Kreditkarte erforderlich

Ein ESP32 BLE MQTT Gateway verbindet die vielen günstigen Bluetooth-Low-Energy-Sensoren in Ihrer Umgebung (Thermometer, Türsensoren, Beacons, Pflanzensensoren) über WLAN mit einem MQTT Broker. In diesem Tutorial bauen Sie auf einem ESP32 einen passiven BLE-Scanner, übernehmen Advertisements sicher aus dem Bluetooth-Task, entfernen Duplikate und veröffentlichen die jeweils neuesten Daten jedes Geräts auf einem eigenen MQTT-Topic.

So funktioniert ein BLE-zu-MQTT-Gateway

Die meisten batteriebetriebenen BLE-Sensoren nehmen nie eine Verbindung an. Sie senden einfach Advertisements: kurze Pakete mit bis zu 31 Byte, die eine Adresse, einen optionalen Namen und Datenfelder wie herstellerspezifische Daten oder Service-Daten enthalten. Ein Sensor, der Temperatur meldet, legt den Messwert typischerweise alle paar Sekunden in eines dieser Felder.

Ein Gateway lauscht auf diese Pakete und leitet sie weiter. Das ist schon der ganze Trick, und er hat zwei große Vorteile:

  • Sensoren bleiben in ihrem stromsparenden Broadcast-Modus, sodass die Batterien lange halten.
  • Ein ESP32 kann viele Geräte gleichzeitig hören, ohne Verbindungen verwalten zu müssen.
Ansatz Wann einsetzen Kompromiss
Passives Scannen von Advertisements Sensoren, die ihre Daten per Broadcast senden Einfach, gut skalierbar, nur lesend
Aktives Scannen (Scan Requests) Sie benötigen die Scan Response, oft den Gerätenamen Etwas mehr Funkverkehr und Stromverbrauch am Sensor
GATT-Verbindungen Gerät stellt Daten nur über Characteristics bereit, oder Sie müssen schreiben Eine Verbindung pro Slot gleichzeitig, mehr Code, mehr Stromverbrauch

Dieses Tutorial nutzt passives Scannen, das die Mehrheit der Sensor-Anwendungsfälle abdeckt.

Was Sie benötigen

  • Ein ESP32-Board mit Bluetooth (der klassische ESP32, ESP32-S3 und ESP32-C3 unterstützen BLE; der ESP32-S2 nicht).
  • Arduino IDE 2.x mit dem esp32-Board-Paket, das die hier verwendete BLE-Bibliothek enthält.
  • Die PubSubClient-Bibliothek.
  • Einen oder mehrere BLE-Sensoren oder eine Smartphone-App, die ein Test-Advertisement senden kann.

WLAN, Bluetooth und MQTT zusammen ergeben eine große Firmware. Schlägt das Kompilieren mit „Sketch too big“ fehl, wählen Sie unter Tools > Partition Scheme eine größere App-Partition, zum Beispiel „Huge APP“.

WLAN und BLE teilen sich ein Funkmodul

Der ESP32 hat ein einziges 2,4-GHz-Funkmodul, das zeitlich zwischen WLAN und Bluetooth aufgeteilt wird. Der Koexistenz-Scheduler regelt das automatisch, doch das hat Folgen:

  • Ein Scan-Fenster von 100 Prozent lässt das WLAN verhungern. Lassen Sie Lücken, indem Sie das Scan-Fenster etwas kürzer als das Intervall einstellen.
  • Während ein blockierender Scan läuft, läuft Ihre MQTT-Schleife nicht. Halten Sie jeden Scan deutlich unter dem MQTT-Keep-Alive-Intervall.
  • Rechnen Sie damit, einige Advertisements zu verpassen. Sensoren wiederholen ihre Broadcasts, daher ist das normalerweise unproblematisch.

Espressif dokumentiert das Koexistenzverhalten im ESP-IDF RF Coexistence Guide.

Topic-Design

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

Doppelpunkte sind in MQTT-Topics erlaubt, der Code entfernt sie aber aus der MAC-Adresse, damit die Topics übersichtlich bleiben und sich im Terminal leicht eintippen lassen. Ersetzen Sie ble-a81f durch Ihre eigene zufällige Kennung.

Der Sketch für das ESP32 BLE MQTT Gateway

Die BLE-Bibliothek ruft den Scan-Callback aus ihrem eigenen Task auf. Aus diesem Callback heraus zu veröffentlichen, hieße, den Netzwerk-Client aus zwei Tasks gleichzeitig anzusprechen, was zu zufälligen Abstürzen führt. Stattdessen kopiert der Callback jedes Advertisement in eine FreeRTOS-Queue, und loop() leert die Queue und veröffentlicht.

#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;    // sehr schwache Geräte ignorieren
const uint32_t MIN_REPUBLISH_MS = 10000; // Ratenbegrenzung pro Gerät

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;

// Einfacher Speicher pro Gerät für die Ratenbegrenzung
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); // Duplikate melden
  scanner->setActiveScan(false);   // passiv: keine Scan Requests senden
  scanner->setInterval(160);       // Einheiten von 0,625 ms
  scanner->setWindow(80);          // Sendezeit für WLAN freilassen
}

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

  scanner->start(SCAN_SECONDS, false);   // blockierender Scan, Ergebnisse landen in der Queue
  scanner->clearResults();               // vom Scan belegten Speicher freigeben

  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();
}

Verfolgen Sie den Datenverkehr von Ihrem Rechner aus:

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

Innerhalb weniger Sekunden sollten Sie eine Zeile pro Gerät in der Nähe sehen. In einer typischen Wohnung oder einem Büro senden Smartphones, Kopfhörer, Fernseher und Uhren Advertisements, rechnen Sie also mit mehr Geräten, als Sie Sensoren besitzen.

Filtern, was Sie weiterleiten

Jedes Advertisement in Reichweite weiterzuleiten, ist für die Erkundung nützlich, im Produktivbetrieb aber zu verrauscht, und es kann Sie bei gemeinsam genutzten Brokern an Ratenlimits stoßen lassen (der öffentliche MQTT-Broker erlaubt 10 Nachrichten pro Sekunde und Client). Drei Filter halten den Datenverkehr unter Kontrolle:

  1. RSSI-Schwelle. MIN_RSSI verwirft Geräte, die weit entfernt oder hinter Wänden sind.
  2. Ratenbegrenzung pro Gerät. MIN_REPUBLISH_MS veröffentlicht jedes Gerät höchstens alle zehn Sekunden, auch wenn es jede Sekunde sendet.
  3. Allowlist. Sobald Sie die Adressen oder Hersteller-IDs Ihrer Sensoren kennen, akzeptieren Sie nur diese. Die ersten zwei Byte der Herstellerdaten sind die Company Identifier, was einen guten Filter für eine bestimmte Marke ergibt.

Beachten Sie, dass viele Smartphones und einige Zubehörgeräte aus Datenschutzgründen zufällige, wechselnde Adressen verwenden, sodass dasselbe Smartphone unter mehreren MAC-Adressen erscheinen kann. Sensoren verwenden in der Regel eine feste Adresse.

Sensordaten dekodieren

Das Gateway leitet Herstellerdaten als Hex weiter und überlässt das Dekodieren einem Subscriber. Das ist Absicht: Firmware-Updates sind aufwendig, während ein Python- oder Node-RED-Subscriber neue Sensorformate in Minuten lernen kann. Viele Open-Source-Sensorfirmwares folgen dokumentierten Formaten wie BTHome-Service-Daten, die ein Subscriber in Temperatur-, Feuchte- und Batteriewerte dekodieren und auf einem sauberen Topic wie home/livingroom/climate erneut veröffentlichen kann.

Diese Aufteilung (einfaches, zuverlässiges Gateway am Edge; flexible Logik in einem Subscriber) ist dasselbe Muster, das größere industrielle Gateways verwenden und das im Artikel zum IIoT-Edge-Gateway beschrieben wird.

Checkliste zur Fehlersuche für das ESP32 BLE MQTT Gateway

  1. Keine Geräte gefunden. Prüfen Sie, ob Ihr Board BLE unterstützt (der ESP32-S2 nicht) und ob der Sensor sendet, etwa mit einer Scanner-App auf dem Smartphone.
  2. MQTT trennt während der Scans. Verkürzen Sie SCAN_SECONDS oder erhöhen Sie den Keep Alive; der Client kann keine Pings beantworten, solange der Scan blockiert.
  3. Zufällige Resets. Prüfen Sie, dass Sie nicht aus dem Scan-Callback heraus veröffentlichen und dass die Queue groß genug ist. Ein wachsender dropped-Zähler bedeutet, dass sie zu klein ist.
  4. WLAN wirkt langsam oder bricht ab. Verkleinern Sie das Scan-Fenster im Verhältnis zum Intervall, damit das WLAN mehr Sendezeit erhält.
  5. Heap schrumpft mit der Zeit. Rufen Sie nach jedem Scan clearResults() auf, wie es der Sketch tut.

Reconnect-Strategien, die Router-Neustarts überstehen, finden Sie unter ESP32 MQTT Reconnect, weitere Sensorprojekte im ESP32-IoT-Tutorial.

Das Gateway absichern

BLE-Advertisements sind von Natur aus öffentlich: Jeder in der Nähe kann sie lesen. Machen Sie es nicht schlimmer, indem Sie sie im Produktivbetrieb auf einem öffentlichen Topic veröffentlichen. Wechseln Sie zu einem privaten Broker auf TLS-Port 8883 mit WiFiClientSecure und Zugangsdaten aus Ihrem Broker-Portal. Ein verwalteter MQTT-Broker wie ConnectMQ gibt jedem Gateway eigene Zugangsdaten, ohne dass Sie Server betreiben müssen. Für Prototypen ist der kostenlose öffentliche MQTT-Broker in Ordnung, solange sich nichts Sensibles in Reichweite befindet; produktive Daten gehören nicht dorthin. In Werkshallen und Lagern, wo BLE-Beacons und Funksensoren etwa Werkzeugwagen oder Umgebungsbedingungen erfassen, sollte jedes Gateway außerdem unter einem eigenen Topic-Zweig entlang der Anlagenhierarchie veröffentlichen, zum Beispiel acme/werk-nuernberg/lager/gateway-2/....

Starten Sie mit ConnectMQ

Wenn das Gateway bereit für den echten Einsatz ist, geben Sie ihm einen privaten, verschlüsselten Endpunkt bei ConnectMQ. Der Live-Topic-Explorer im Portal zeigt jedes Geräte-Topic, sobald es erscheint, und ist damit der schnellste Weg zu sehen, was Ihr Gateway empfängt. Starten Sie mit dem kostenlosen Serverless-Plan unter app.connectmq.com, ganz ohne Kreditkarte.

Häufig gestellte Fragen

Wie viele BLE-Sensoren kann ein ESP32-Gateway verarbeiten?

Beim passiven Scannen sind Dutzende Geräte in Reichweite normal; die praktischen Grenzen sind Sendezeit, Queue-Größe und die Häufigkeit Ihrer Veröffentlichungen. Begrenzen Sie die Rate pro Gerät und filtern Sie auf die Sensoren, die Sie wirklich interessieren.

Sollte ich NimBLE statt der integrierten BLE-Bibliothek verwenden?

NimBLE-Arduino verbraucht weniger Speicher und ist eine gute Wahl für größere Gateways. Die API unterscheidet sich leicht, passen Sie also den Callback-Code an, wenn Sie wechseln.

Kann sich das Gateway mit Sensoren verbinden, die ein Pairing erfordern?

Ja, dafür sind aber GATT-Verbindungen statt Scannen nötig, und es sind nur wenige Verbindungen gleichzeitig möglich. Für sendende Sensoren ist passives Scannen einfacher und zuverlässiger.

Warum sehe ich Geräte, die mir nicht gehören?

Smartphones, Fernseher, Kopfhörer und viele andere Produkte senden ständig Advertisements. Nutzen Sie eine RSSI-Schwelle und eine Allowlist von Adressen oder Hersteller-IDs.

Verwandte Artikel

ESP32 NTP MQTT Zeitstempel: genaue Uhrzeit in jeder Nachricht

Synchronisieren Sie die ESP32-Uhr per NTP, formatieren Sie Zeitstempel in ISO 8601 und Epoch-Millisekunden und hängen Sie sie an MQTT-Payloads, damit Messwerte ihren echten Erfassungszeitpunkt behalten, auch wenn sie verspätet veröffentlicht werden.

8 Min. Lesezeit

Alle Artikel →

Ihr privater MQTT-Broker ist nur wenige Klicks entfernt

Konto anlegen, Broker-Zugangsdaten erhalten und das erste Gerät verbinden — ohne Server, die installiert, gepatcht oder skaliert werden müssen.