ESP32 et Arduino

Passerelle BLE MQTT ESP32 : scanner les capteurs Bluetooth et relayer leurs annonces

Utilisez un ESP32 comme scanner Bluetooth Low Energy qui collecte les annonces des capteurs et balises à proximité, les filtre, élimine les doublons et les publie sur un topic MQTT par appareil.

Passerelle BLE MQTT ESP32 recevant les annonces Bluetooth de capteurs proches et les relayant en Wi-Fi
Essayez-le sur votre propre broker MQTTObtenez un broker MQTT privé et managé avec l’offre Free et partez d’exemples de code prêts à l’emploi.
Essayer gratuitement Exemples de code Aucune carte bancaire requise

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 :

  1. Seuil RSSI. MIN_RSSI écarte les appareils éloignés ou derrière des murs.
  2. Limite de débit par appareil. MIN_REPUBLISH_MS publie chaque appareil au plus une fois toutes les dix secondes, même s'il émet une annonce chaque seconde.
  3. 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

  1. 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.
  2. Déconnexions MQTT pendant les scans. Raccourcissez SCAN_SECONDS ou augmentez le keep alive ; le client ne peut pas répondre aux pings pendant que le scan bloque.
  3. 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 dropped qui augmente signifie qu'elle est trop petite.
  4. 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.
  5. 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.

Articles liés

Exemple MQTT ESP-IDF : construire un client esp-mqtt à partir de zéro

Construisez un projet ESP-IDF à partir d'un dossier vide avec le composant esp-mqtt : configuration du Wi-Fi en mode station, gestionnaire d'événements MQTT, publication et abonnement, propriétés MQTT 5 et TLS avec le bundle de certificats.

9 min de lecture

Horodatage NTP ESP32 MQTT : ajouter une heure précise à chaque message

Synchronisez l'horloge de l'ESP32 avec NTP, formatez des horodatages ISO 8601 et epoch en millisecondes, et joignez-les aux payloads MQTT pour que les mesures conservent leur véritable heure de capture même lorsqu'elles sont publiées en retard.

8 min de lecture

Tous les articles →

Votre broker MQTT privé, à quelques clics

Créez un compte, récupérez les identifiants de votre broker et connectez votre premier appareil — aucun serveur à installer, mettre à jour ou dimensionner.