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:
- RSSI-Schwelle.
MIN_RSSIverwirft Geräte, die weit entfernt oder hinter Wänden sind. - Ratenbegrenzung pro Gerät.
MIN_REPUBLISH_MSveröffentlicht jedes Gerät höchstens alle zehn Sekunden, auch wenn es jede Sekunde sendet. - 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
- 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.
- MQTT trennt während der Scans. Verkürzen Sie
SCAN_SECONDSoder erhöhen Sie den Keep Alive; der Client kann keine Pings beantworten, solange der Scan blockiert. - 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. - WLAN wirkt langsam oder bricht ab. Verkleinern Sie das Scan-Fenster im Verhältnis zum Intervall, damit das WLAN mehr Sendezeit erhält.
- 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.



