Une passerelle BLE MQTT ESP32 relie les nombreux capteurs Bluetooth Low Energy bon marché qui vous entourent (thermomètres, capteurs d'ouverture, balises, moniteurs de plantes) à un broker MQTT via le Wi-Fi, ce qui en fait une brique pratique pour la maison connectée. Dans ce tutoriel, vous allez construire un scanner BLE passif sur un ESP32, récupérer les annonces en toute sécurité depuis la tâche Bluetooth, éliminer les doublons et publier les dernières données de chaque appareil sur son propre topic MQTT. Une fois les données sur MQTT, Home Assistant, Jeedom ou Domoticz peuvent s'y abonner comme n'importe quel autre client.
Fonctionnement d'une passerelle BLE vers MQTT
La plupart des capteurs BLE sur pile n'acceptent jamais de connexion. Ils diffusent simplement des annonces (advertisements) : de courts paquets de 31 octets maximum contenant une adresse, un nom facultatif et des champs de données comme les données constructeur (manufacturer data) ou les données de service. Un capteur de température place généralement sa mesure dans l'un de ces champs toutes les quelques secondes.
Une passerelle écoute ces paquets et les relaie. Tout le principe est là, et il présente deux gros avantages :
- Les capteurs restent dans leur mode de diffusion basse consommation, si bien que les piles durent longtemps.
- Un seul ESP32 peut entendre de nombreux appareils à la fois sans gérer de connexions.
| Approche | Quand l'utiliser | Contrepartie |
|---|---|---|
| Scan passif des annonces | Capteurs qui diffusent leurs données | Simple, monte bien en charge, lecture seule |
| Scan actif (scan requests) | Vous avez besoin de la scan response, souvent le nom de l'appareil | Un peu plus de trafic radio et de consommation côté capteur |
| Connexions GATT | L'appareil n'expose ses données que via des caractéristiques, ou vous devez écrire | Une connexion à la fois par emplacement, plus de code, plus de consommation |
Ce tutoriel utilise le scan passif, qui couvre la majorité des cas d'usage de capteurs.
Ce qu'il vous faut
- Une carte ESP32 avec Bluetooth (l'ESP32 classique, l'ESP32-S3 et l'ESP32-C3 prennent tous en charge le BLE ; l'ESP32-S2 non).
- Arduino IDE 2.x avec le paquet de cartes esp32, qui inclut la bibliothèque BLE utilisée ici.
- La bibliothèque PubSubClient.
- Un ou plusieurs capteurs BLE, ou une application mobile capable de diffuser une annonce de test.
Wi-Fi, Bluetooth et MQTT réunis donnent un firmware volumineux. Si la compilation échoue avec « Sketch too big », choisissez une partition d'application plus grande dans Tools > Partition Scheme, par exemple « Huge APP ».
Wi-Fi et BLE partagent une seule radio
L'ESP32 dispose d'une seule radio 2,4 GHz, partagée dans le temps entre le Wi-Fi et le Bluetooth. L'ordonnanceur de coexistence gère cela automatiquement, mais avec des conséquences :
- Une fenêtre de scan à 100 % affame le Wi-Fi. Laissez des trous en réglant la fenêtre de scan un peu plus courte que l'intervalle.
- Pendant qu'un scan bloquant s'exécute, votre boucle MQTT ne tourne pas. Gardez chaque scan nettement plus court que l'intervalle keep alive MQTT.
- Attendez-vous à manquer quelques annonces. Les capteurs répètent leurs diffusions, ce qui est donc normalement sans conséquence.
Espressif documente le comportement de coexistence dans le guide de coexistence RF d'ESP-IDF.
Conception des topics
| Topic | Payload |
|---|---|
connectmq/demo/ble-a81f/devices/<mac> |
{"rssi":-71,"name":"LYWSD03","mfg":"...","svc":"..."} |
connectmq/demo/ble-a81f/gateway/status |
online / offline, retenu (Last Will) |
connectmq/demo/ble-a81f/gateway/stats |
{"seen":12,"published":9,"dropped":0} |
Les deux-points sont autorisés dans les topics MQTT, mais le code les retire de l'adresse MAC pour garder des topics propres et faciles à taper dans un terminal. Remplacez ble-a81f par votre propre identifiant aléatoire.
Le sketch de la passerelle BLE MQTT ESP32
La bibliothèque BLE appelle le callback de scan depuis sa propre tâche. Publier depuis ce callback reviendrait à manipuler le client réseau depuis deux tâches à la fois, ce qui provoque des plantages aléatoires. À la place, le callback copie chaque annonce dans une file FreeRTOS, et loop() vide la file et publie.
#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; // ignorer les appareils très faibles
const uint32_t MIN_REPUBLISH_MS = 10000; // limite de débit par appareil
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;
// Mémoire simple par appareil pour la limitation de débit
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); // signaler les doublons
scanner->setActiveScan(false); // passif : pas de scan requests
scanner->setInterval(160); // unités de 0,625 ms
scanner->setWindow(80); // laisser du temps d'antenne au Wi-Fi
}
void loop() {
if (WiFi.status() != WL_CONNECTED) connectWifi();
if (!mqtt.connected()) connectMqtt();
scanner->start(SCAN_SECONDS, false); // scan bloquant, les résultats vont dans la file
scanner->clearResults(); // libérer la mémoire occupée par le scan
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();
}
Observez le trafic depuis votre ordinateur :
mosquitto_sub -h broker.connectmq.com -p 1883 -i ble-a81f-viewer -t "connectmq/demo/ble-a81f/#" -v
En quelques secondes, vous devriez voir une ligne par appareil à proximité. Dans une maison ou un bureau classique, téléphones, casques, téléviseurs et montres émettent tous des annonces : attendez-vous donc à voir plus d'appareils que vous n'avez de capteurs.
Filtrer ce que vous relayez
Relayer toutes les annonces à portée est utile pour la découverte mais bruyant en production, et peut vous faire atteindre les limites de débit des brokers partagés (la zone de test publique autorise 10 messages par seconde et par client). Trois filtres permettent de maîtriser le trafic :
- Seuil RSSI.
MIN_RSSIécarte les appareils éloignés ou derrière des murs. - Limite de débit par appareil.
MIN_REPUBLISH_MSpublie chaque appareil au plus une fois toutes les dix secondes, même s'il émet une annonce chaque seconde. - Liste blanche. Une fois que vous connaissez les adresses ou les identifiants constructeur de vos capteurs, n'acceptez que ceux-là. Les deux premiers octets des données constructeur correspondent à l'identifiant de l'entreprise, ce qui fait un bon filtre pour une marque donnée.
Notez que de nombreux téléphones et certains accessoires utilisent des adresses aléatoires changeantes pour protéger la vie privée : le même téléphone peut donc apparaître sous plusieurs adresses MAC. Les capteurs utilisent généralement une adresse fixe.
Décoder les données des capteurs
La passerelle relaie les données constructeur en hexadécimal et laisse le décodage à un abonné. C'est volontaire : mettre à jour un firmware est difficile, alors qu'un abonné Python ou Node-RED peut apprendre de nouveaux formats de capteurs en quelques minutes. De nombreux firmwares de capteurs open source suivent des formats documentés, comme les données de service BTHome, qu'un abonné peut décoder en température, humidité et niveau de batterie avant de les republier sur un topic propre comme home/livingroom/climate.
Cette répartition (une passerelle simple et fiable en périphérie, une logique flexible dans un abonné) est le même schéma que celui des passerelles industrielles plus importantes, décrit dans l'article sur la passerelle edge IIoT.
Check-list de dépannage de la passerelle BLE MQTT ESP32
- Aucun appareil détecté. Vérifiez que votre carte prend en charge le BLE (l'ESP32-S2 non) et que le capteur émet bien des annonces, à l'aide d'une application de scan sur téléphone.
- Déconnexions MQTT pendant les scans. Raccourcissez
SCAN_SECONDSou augmentez le keep alive ; le client ne peut pas répondre aux pings pendant que le scan bloque. - Redémarrages aléatoires. Vérifiez que vous ne publiez pas depuis le callback de scan et que la file est assez grande. Un compteur
droppedqui augmente signifie qu'elle est trop petite. - Wi-Fi lent ou qui décroche. Réduisez la fenêtre de scan par rapport à l'intervalle pour donner plus de temps d'antenne au Wi-Fi.
- Tas qui diminue avec le temps. Appelez
clearResults()après chaque scan, comme le fait le sketch.
Pour des stratégies de reconnexion qui survivent aux redémarrages du routeur, consultez ESP32 MQTT reconnect, et pour d'autres projets de capteurs le tutoriel IoT ESP32.
Sécuriser la passerelle
Les annonces BLE sont publiques par nature : n'importe qui à proximité peut les lire. N'aggravez pas la situation en les publiant sur un topic public en production. Passez à un broker privé sur le port TLS 8883 avec WiFiClientSecure et les identifiants fournis par le portail de votre broker. Un broker MQTT managé comme ConnectMQ donne à chaque passerelle ses propres identifiants sans exploiter de serveurs ; pendant le prototypage, le serveur MQTT public gratuit convient tant que rien de sensible n'est à portée.
Démarrer avec ConnectMQ
Quand la passerelle est prête pour un usage réel, donnez-lui un point de connexion privé et chiffré sur ConnectMQ. L'explorateur de topics en direct du portail affiche chaque topic d'appareil dès son apparition : c'est le moyen le plus rapide de voir ce que votre passerelle entend. Commencez avec l'offre Serverless gratuite sur app.connectmq.com, sans carte bancaire.
Questions fréquentes
Combien de capteurs BLE une passerelle ESP32 peut-elle gérer ?
En scan passif, avoir des dizaines d'appareils à portée est normal ; les limites pratiques sont le temps d'antenne, la taille de la file et la fréquence de publication. Limitez le débit par appareil et filtrez sur les capteurs qui vous intéressent.
Faut-il utiliser NimBLE plutôt que la bibliothèque BLE intégrée ?
NimBLE-Arduino consomme moins de mémoire et constitue un bon choix pour les passerelles plus importantes. Son API diffère légèrement : adaptez le code du callback si vous changez.
La passerelle peut-elle se connecter à des capteurs qui exigent un appairage ?
Oui, mais cela nécessite des connexions GATT plutôt qu'un scan, et seulement quelques connexions simultanées. Pour les capteurs qui diffusent, le scan passif est plus simple et plus fiable.
Pourquoi vois-je des appareils qui ne m'appartiennent pas ?
Téléphones, téléviseurs, casques et bien d'autres produits émettent des annonces en continu. Utilisez un seuil RSSI et une liste blanche d'adresses ou d'identifiants constructeur.



