Un afficheur ESP32 OLED MQTT est le moyen le plus simple d'afficher des données en direct sur un bureau, un établi ou un panneau de machine, sans téléphone ni navigateur. Dans ce tutoriel, vous allez vous abonner à plusieurs topics sur broker.connectmq.com, afficher les dernières valeurs sur un écran SSD1306 de 128×64, marquer les mesures comme périmées lorsqu'un capteur cesse d'émettre, et afficher l'état Wi-Fi et MQTT de l'afficheur lui-même. Le même sketch fonctionne sans modification lorsque vous passez à des identifiants privés.
Ce qu'affiche l'écran
Un bon mini tableau de bord répond d'un coup d'œil à trois questions : quelle est la valeur, quel est son âge, et l'afficheur lui-même est-il connecté ? La disposition utilisée ici fait tenir les trois sur un écran de 0,96 pouce :
+--------------------------------+
| MQTT OK RSSI -58 |
| Temp 23.4 C |
| Humidity 48 % |
| Door CLOSED (12s) |
| Msg: Coffee is ready |
+--------------------------------+
Chaque ligne est associée à un topic. L'afficheur s'abonne avec un seul wildcard et répartit les messages entrants dans les lignes en comparant le dernier niveau du topic.
| Topic | Ligne | Exemple de payload |
|---|---|---|
connectmq/demo/<id>/temp |
Temp | 23.4 |
connectmq/demo/<id>/humidity |
Humidity | 48 |
connectmq/demo/<id>/door |
Door | CLOSED |
connectmq/demo/<id>/msg |
Msg | N'importe quel texte court |
Dans une vraie installation, les valeurs proviennent d'autres appareils : un ESP32 avec un capteur DHT22 qui publie la température et l'humidité, un contact de porte ou un script qui envoie des notifications. L'afficheur ne se soucie pas de qui publie, et c'est la principale force du modèle publication/abonnement. Si ce découplage est nouveau pour vous, lisez ce que fait un broker MQTT avant de commencer.
Matériel et câblage
- Une carte de développement ESP32.
- Un module OLED SSD1306, 128×64 pixels, version I2C (quatre broches : GND, VCC, SCL, SDA).
- Quatre fils de liaison.
| Broche OLED | Broche ESP32 |
|---|---|
| GND | GND |
| VCC | 3V3 |
| SCL | GPIO 22 |
| SDA | GPIO 21 |
GPIO 21 et 22 sont les broches I2C par défaut des cartes ESP32 DevKit classiques. D'autres variantes comme l'ESP32-S3 ou le C3 utilisent d'autres broches par défaut : passez donc explicitement vos broches à Wire.begin(SDA, SCL), comme le fait le sketch. La plupart des modules utilisent l'adresse I2C 0x3C ; quelques-uns utilisent 0x3D. Si l'écran reste noir, lancez un sketch de scan I2C pour confirmer l'adresse.
Bibliothèques
Si vous n'avez jamais flashé un ESP32 depuis l'Arduino IDE, suivez d'abord la configuration de la carte dans le tutoriel IoT ESP32 ; il explique comment installer le paquet de cartes esp32, choisir la bonne carte et le bon port, et lire le moniteur série, autant d'éléments dont vous aurez besoin ici. Installez ensuite via le gestionnaire de bibliothèques Arduino :
- PubSubClient de Nick O'Leary.
- Adafruit SSD1306 d'Adafruit, qui installe aussi Adafruit GFX Library et Adafruit BusIO.
Le sketch ESP32 OLED MQTT
La règle de conception essentielle : ne jamais dessiner dans le callback MQTT. Le callback se contente d'enregistrer la valeur et un horodatage, puis de lever un indicateur. Le dessin a lieu dans loop() à cadence fixe. Le callback reste ainsi rapide, on évite les écrans à moitié mis à jour, et l'afficheur peut actualiser les compteurs d'« âge » même en l'absence de messages.
#include <WiFi.h>
#include <Wire.h>
#include <PubSubClient.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.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 = "oled-5d20ab"; // unique
const char* BASE = "connectmq/demo/oled-5d20ab";
const unsigned long STALE_MS = 120000; // 2 minutes
Adafruit_SSD1306 display(128, 64, &Wire, -1);
WiFiClient net;
PubSubClient mqtt(net);
struct Row {
const char* key; // dernier niveau du topic
const char* label;
const char* unit;
String value;
unsigned long at; // millis() à la réception, 0 = jamais
};
Row rows[] = {
{"temp", "Temp", " C", "", 0},
{"humidity", "Humidity", " %", "", 0},
{"door", "Door", "", "", 0},
{"msg", "Msg:", "", "", 0},
};
const int ROW_COUNT = sizeof(rows) / sizeof(rows[0]);
bool dirty = true;
unsigned long lastDraw = 0, lastAttempt = 0;
void onMessage(char* topic, byte* payload, unsigned int length) {
const char* leaf = strrchr(topic, '/');
if (!leaf) return;
leaf++;
for (int i = 0; i < ROW_COUNT; i++) {
if (strcmp(leaf, rows[i].key) == 0) {
String v;
for (unsigned int j = 0; j < length && j < 40; j++) v += (char)payload[j];
rows[i].value = v;
rows[i].at = millis();
dirty = true;
return;
}
}
}
String ageText(unsigned long at) {
unsigned long s = (millis() - at) / 1000;
if (s < 60) return String(s) + "s";
if (s < 3600) return String(s / 60) + "m";
return String(s / 3600) + "h";
}
void draw() {
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
// Barre d'état
display.setCursor(0, 0);
display.print("MQTT ");
display.print(mqtt.connected() ? "OK" : (WiFi.status() == WL_CONNECTED ? "..." : "NO WIFI"));
if (WiFi.status() == WL_CONNECTED) {
display.setCursor(80, 0);
display.print("RSSI ");
display.print(WiFi.RSSI());
}
display.drawFastHLine(0, 10, 128, SSD1306_WHITE);
int y = 14;
for (int i = 0; i < ROW_COUNT; i++) {
display.setCursor(0, y);
display.print(rows[i].label);
display.setCursor(i == 3 ? 30 : 60, y);
if (rows[i].at == 0) {
display.print("--");
} else {
bool stale = millis() - rows[i].at > STALE_MS;
String text = rows[i].value + rows[i].unit;
if (i == 3) text = text.substring(0, 16); // adapter à la ligne de message
display.print(text);
if (stale && i != 3) {
display.setCursor(104, y);
display.print(ageText(rows[i].at)); // n'afficher l'âge que si la valeur est périmée
}
}
y += 12;
}
display.display();
}
bool connectMqtt() {
if (!mqtt.connect(CLIENT_ID)) {
Serial.printf("MQTT failed, state=%d\n", mqtt.state());
return false;
}
String filter = String(BASE) + "/+";
mqtt.subscribe(filter.c_str(), 1);
Serial.println("Subscribed to " + filter);
return true;
}
void setup() {
Serial.begin(115200);
Wire.begin(21, 22);
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println("SSD1306 not found");
while (true) delay(1000);
}
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.print("Connecting...");
display.display();
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
mqtt.setServer(MQTT_HOST, MQTT_PORT);
mqtt.setCallback(onMessage);
mqtt.setSocketTimeout(5);
}
void loop() {
if (WiFi.status() == WL_CONNECTED) {
if (!mqtt.connected()) {
if (millis() - lastAttempt > 5000) { lastAttempt = millis(); connectMqtt(); }
} else {
mqtt.loop();
}
}
// Redessiner à l'arrivée de nouvelles données, et une fois par seconde pour l'état et les âges
if (dirty || millis() - lastDraw > 1000) {
draw();
dirty = false;
lastDraw = millis();
}
}
Pourquoi les retained messages comptent pour un afficheur
Au démarrage, l'ESP32 ne sait rien. Si les capteurs publient leurs mesures sous forme de retained messages, le broker transmet à l'afficheur la dernière valeur de chaque topic dès l'abonnement, et l'écran est rempli en moins d'une seconde. Sans retained messages, les lignes affichent -- jusqu'à la prochaine publication de chaque capteur, ce qui peut prendre des minutes pour un appareil sur batterie. Si vous maîtrisez les publieurs, retenez les valeurs qui évoluent lentement, comme la température, l'état de la porte et la dernière notification.
Tester depuis la ligne de commande
Publiez quelques valeurs comme si vous étiez les capteurs :
mosquitto_pub -h broker.connectmq.com -p 1883 -i pub-5d20ab -r -t "connectmq/demo/oled-5d20ab/temp" -m "23.4"
mosquitto_pub -h broker.connectmq.com -p 1883 -i pub-5d20ab -r -t "connectmq/demo/oled-5d20ab/humidity" -m "48"
mosquitto_pub -h broker.connectmq.com -p 1883 -i pub-5d20ab -r -t "connectmq/demo/oled-5d20ab/door" -m "CLOSED"
mosquitto_pub -h broker.connectmq.com -p 1883 -i pub-5d20ab -t "connectmq/demo/oled-5d20ab/msg" -m "Coffee is ready"
Appuyez maintenant sur le bouton de réinitialisation de l'ESP32. Comme les trois premières valeurs ont été envoyées avec -r, elles réapparaissent presque immédiatement après la reconnexion ; la ligne de message reste vide car elle n'était pas retenue. Attendez deux minutes sans publier : l'indicateur d'âge apparaît à côté de chaque valeur périmée.
Pour effacer une valeur retenue, publiez un message retenu vide sur ce topic :
mosquitto_pub -h broker.connectmq.com -p 1883 -i pub-5d20ab -r -n -t "connectmq/demo/oled-5d20ab/door"
Les données retenues sur le serveur MQTT public gratuit sont visibles par quiconque s'abonne : limitez-vous donc à des valeurs de démonstration inoffensives.
Choisir les topics d'un afficheur partagé
La démo place chaque ligne sous le préfixe propre à l'afficheur, ce qui rend l'exemple autonome. Dans une vraie installation, ce sont généralement les capteurs qui possèdent leurs topics, par exemple home/livingroom/temp pour un appareil et home/garage/door pour un autre. Deux options s'offrent à vous. La première consiste à s'abonner explicitement à chaque topic et à associer les topics complets aux lignes : c'est l'approche la plus prévisible, à privilégier quand les topics proviennent de différentes branches de l'arborescence. La seconde consiste à adopter une convention, comme une branche display/ dans laquelle les capteurs ou un petit flux Node-RED republient. Le firmware de l'afficheur reste ainsi générique, et ajouter une ligne devient un changement de configuration plutôt qu'un changement de code. Quel que soit votre choix, n'oubliez pas que la zone publique autorise 10 abonnements par client : une poignée de topics explicites convient, des dizaines non.
Conseils de lisibilité pour les petits écrans
- Utilisez du texte de taille 2 pour le nombre le plus important. Un écran de 128×64 accueille environ quatre lignes de texte en taille 2, assez pour une grande valeur plus une ligne d'état.
- Alignez les nombres à droite pour que les chiffres ne sautent pas quand les valeurs changent. Mesurez la largeur du texte avec
display.getTextBounds(). - Arrondissez côté publieur, pas côté afficheur. Envoyer
23.4au lieu de23.41678économise des octets et de la place à l'écran. - Signalez explicitement les données périmées. Une valeur figée qui semble actuelle est pire qu'une valeur vide. Le compteur d'âge est l'indicateur honnête le moins coûteux.
- Attention au marquage de l'écran. Les pixels OLED vieillissent à l'usage. Pour les panneaux toujours allumés, réduisez le contraste avec
display.dim(true)ou décalez la mise en page d'un pixel toutes les quelques minutes.
Dépannage
- Écran noir, aucune erreur. Mauvaise adresse I2C ou SDA/SCL inversés. Lancez un scanner I2C.
- « SSD1306 not found » sur une carte S3 ou C3. Les broches par défaut diffèrent ; passez les bonnes à
Wire.begin(). - Texte illisible avec une image décalée. Vous avez peut-être un module 128×32 ; modifiez la hauteur dans le constructeur.
- Les valeurs ne se mettent jamais à jour mais l'état indique OK. Topic qui ne correspond pas. Le wildcard
+correspond à exactement un niveau :connectmq/demo/oled-5d20ab/sensors/tempne correspondrait donc pas. Abonnez-vous à#pendant le débogage. - L'afficheur se fige pendant plusieurs secondes. Une reconnexion bloquante. Réduisez le délai du socket ou utilisez le schéma d'attente progressive non bloquant de la logique de reconnexion MQTT sur ESP32.
Du panneau de démo à l'afficheur permanent
Un afficheur accroché au mur d'un atelier ne doit pas dépendre du serveur MQTT public gratuit, conçu pour les tests. Créez un broker dans le portail ConnectMQ, connectez-vous avec le nom d'utilisateur, le mot de passe et le client ID générés en TLS sur le port 8883, et faites pointer tous vos capteurs vers le même point d'accès. Un broker MQTT cloud permet au panneau de la maison d'afficher les mesures d'une serre située à l'autre bout de la ville, sans aucune redirection de port.
ConnectMQ, un broker MQTT managé, facilite aussi le débogage : l'explorateur de topics en direct montre exactement quels topics existent et ce qui est retenu sur chacun, ce qui répond généralement en quelques secondes à la question « pourquoi cette ligne est-elle vide ? ». Pour une sortie plus visuelle à côté du texte, associez l'OLED à un ruban LED WS2812B piloté par MQTT servant de voyant d'état à code couleur.
Démarrer avec ConnectMQ
Ouvrez app.connectmq.com, choisissez l'offre Serverless gratuite (sans carte bancaire) et créez un broker. Votre afficheur, vos capteurs et votre téléphone peuvent alors partager un même point d'accès sécurisé, et vous pouvez suivre chaque message dans le portail pendant la construction.
Questions fréquentes
L'écran OLED peut-il s'abonner à des topics provenant d'autres appareils ?
Oui. C'est tout l'intérêt de MQTT : n'importe quel appareil peut publier sur un topic, et n'importe quel nombre d'abonnés, y compris cet afficheur, le reçoit. Seuls les noms de topics doivent correspondre.
Pourquoi ne pas dessiner sur l'OLED dans le callback MQTT ?
Dessiner prend plusieurs millisecondes en I2C, et plusieurs messages peuvent arriver en rafale. Enregistrer les valeurs dans le callback et dessiner à cadence fixe dans loop() maintient la réactivité du client et la cohérence de l'écran.
Cela fonctionne-t-il avec les écrans SH1106 ?
Les modules de 1,3 pouce utilisent souvent le contrôleur SH1106. Utilisez une bibliothèque qui le prend en charge, comme U8g2 ou un fork SH1106 du pilote Adafruit ; la partie MQTT du sketch reste identique.
Comment afficher une valeur issue d'un payload JSON ?
Analysez le payload avec ArduinoJson dans le callback, extrayez le champ voulu et enregistrez-le comme valeur de la ligne. Gardez l'analyse dans le callback courte et le dessin dans loop().



