Gegevens versturen met MQTT
Onze ESP32 kan inmiddels zelfstandig de BME280 uitlezen en verbinding maken met WiFi. Wanneer het netwerk tijdelijk ontbreekt, blijft de sensor meten en…
Onze ESP32 kan inmiddels zelfstandig de BME280 uitlezen en verbinding maken met WiFi. Wanneer het netwerk tijdelijk ontbreekt, blijft de sensor meten en probeert de ESP32 later opnieuw verbinding te maken.
Nu willen we de meetwaarden beschikbaar maken voor andere apparaten.
Daarvoor gebruiken we MQTT.
Via MQTT kan de ESP32 onder andere:
- temperatuur publiceren;
- luchtvochtigheid publiceren;
- luchtdruk publiceren;
- de eigen beschikbaarheid melden;
- opdrachten van andere apparaten ontvangen;
- gegevens beschikbaar maken voor Home Assistant.
Ook bij MQTT houden we vast aan hetzelfde uitgangspunt als in het vorige hoofdstuk:
Een ontbrekende netwerkverbinding of MQTT-broker mag de lokale werking van de ESP32 niet stilzetten.
De sensor blijft meten wanneer MQTT niet beschikbaar is. De ESP32 probeert regelmatig opnieuw verbinding te maken. Zodra de broker terugkomt, wordt de verbinding automatisch hersteld en worden de meetwaarden weer gepubliceerd.
Deze werkwijze is gebaseerd op de failsafe-code die ik zelf als basis voor veel ESP32-projecten gebruik.
Wat MQTT eigenlijk doet
MQTT is een communicatieprotocol voor het uitwisselen van berichten tussen apparaten.
De letters staan oorspronkelijk voor Message Queuing Telemetry Transport. Voor ons is vooral belangrijk dat MQTT een lichte en overzichtelijke manier biedt om meetwaarden en opdrachten over een netwerk te versturen.
Bij MQTT sturen apparaten hun berichten niet rechtstreeks naar elkaar. De communicatie loopt via een centrale tussenpersoon:
de MQTT-broker
De broker ontvangt berichten en geeft ze door aan apparaten of programma’s die belangstelling voor die berichten hebben.
De verbinding ziet er daardoor zo uit:
ESP32 → WiFi-netwerk → MQTT-broker → ontvanger
Een ontvanger kan bijvoorbeeld zijn:
- Home Assistant;
- een tweede ESP32;
- een computer;
- een dashboard;
- een telefoonapp;
- een programma dat meetwaarden opslaat.
De broker als tussenpersoon
Je kunt de broker vergelijken met een postkantoor.
De ESP32 brengt een bericht naar het postkantoor en vermeldt voor welk onderwerp het bestemd is. Andere apparaten kunnen bij het postkantoor aangeven welke onderwerpen ze willen ontvangen.
De ESP32 hoeft daardoor niet te weten:
- hoeveel ontvangers er zijn;
- welke IP-adressen de ontvangers hebben;
- of Home Assistant op dat moment naar het scherm kijkt;
- wat de ontvangers uiteindelijk met het bericht doen.
De ESP32 hoeft alleen de broker te kunnen bereiken.
De broker zorgt voor de verdere verdeling.
Publiceren en abonneren
MQTT gebruikt twee belangrijke handelingen:
- publiceren;
- abonneren.
Publiceren
Wanneer de ESP32 een meetwaarde verstuurt, noemen we dat publishen of publiceren.
De ESP32 publiceert bijvoorbeeld de waarde:
21.8
op het onderwerp:
werkplaats/sensor/temperatuur
Abonneren
Een apparaat dat berichten over een bepaald onderwerp wil ontvangen, abonneert zich op dat onderwerp.
Home Assistant kan zich bijvoorbeeld abonneren op:
werkplaats/sensor/temperatuur
Wanneer de ESP32 een nieuwe waarde publiceert, stuurt de broker die waarde door naar Home Assistant.
Topics
Een MQTT-onderwerp wordt een topic genoemd.
Topics bestaan vaak uit verschillende niveaus, gescheiden door schuine strepen:
werkplaats/sensor/temperatuur
Je kunt de niveaus lezen als een route:
werkplaats
└── sensor
└── temperatuur
Voor ons project gebruiken we:
werkplaats/sensor/temperatuur
werkplaats/sensor/luchtvochtigheid
werkplaats/sensor/luchtdruk
werkplaats/sensor/wifi-rssi
werkplaats/sensor/status
werkplaats/sensor/opdracht
Deze structuur maakt direct duidelijk:
- bij welk project het bericht hoort;
- welk apparaat de gegevens verstuurt;
- om welke meetwaarde of functie het gaat.
MQTT schrijft zelf niet voor hoe jij je topics moet noemen. Juist daarom is het verstandig vooraf een herkenbare structuur te kiezen.
Hoofdletters maken verschil
MQTT-topics zijn hoofdlettergevoelig.
Deze topics zijn dus niet hetzelfde:
werkplaats/sensor/temperatuur
Werkplaats/sensor/temperatuur
werkplaats/Sensor/Temperatuur
Een abonnement op het eerste topic ontvangt geen berichten die op het tweede topic worden gepubliceerd.
Gebruik daarom consequent dezelfde schrijfwijze. In deze gids gebruiken we kleine letters.
Wat hebben we nodig?
Voor dit hoofdstuk heb je nodig:
- de ESP32 met BME280 uit de vorige hoofdstukken;
- een werkende 2,4GHz-WiFi-verbinding;
- het IP-adres of de hostnaam van een MQTT-broker;
- eventueel een gebruikersnaam en wachtwoord voor de broker;
- de Arduino IDE;
- de PubSubClient-bibliotheek.
De MQTT-broker kan bijvoorbeeld draaien:
- als Mosquitto-broker binnen Home Assistant;
- op een Raspberry Pi;
- op een computer;
- op een NAS;
- op een server binnen je netwerk;
- bij een externe MQTT-dienst.
In deze gids gaan we uit van een broker binnen je eigen netwerk die bereikbaar is via poort 1883 en een gebruikersnaam en wachtwoord gebruikt.
MQTT binnen Home Assistant
Gebruik je Home Assistant, dan kun je bijvoorbeeld de Mosquitto broker add-on installeren en via de MQTT-integratie met Home Assistant koppelen.
De precieze installatie hangt af van jouw Home Assistant-installatie. Een Home Assistant OS-installatie biedt andere mogelijkheden dan een losse Container- of Core-installatie.
Voor onze ESP32-code hebben we uiteindelijk vier gegevens nodig:
- het adres van de broker;
- de gebruikte poort;
- de MQTT-gebruikersnaam;
- het MQTT-wachtwoord.
Gebruik bij voorkeur een afzonderlijk account voor MQTT-apparaten. Gebruik niet zonder noodzaak het beheerdersaccount van Home Assistant.
Het adres van de broker
Een broker binnen je lokale netwerk heeft meestal een adres zoals:
192.168.1.50
of een lokale hostnaam zoals:
homeassistant.local
Voor een eerste test is een vast lokaal IP-adres vaak het duidelijkst. Het voorkomt dat naamomzetting een extra mogelijke foutoorzaak wordt.
Let erop dat je het adres van de broker gebruikt en niet het IP-adres van de ESP32.
De richting is:
De ESP32 maakt verbinding met het adres van de broker.
Poort 1883
MQTT zonder TLS gebruikt meestal TCP-poort:
1883
In onze code stellen we daarom in:
const int mqttPort = 1883;
Deze verbinding is niet automatisch versleuteld.
Een gebruikersnaam en wachtwoord zorgen voor authenticatie, maar maken het netwerkverkeer op zichzelf nog niet versleuteld. Gebruik deze eenvoudige verbinding daarom alleen binnen een netwerk dat je vertrouwt.
Voor MQTT via internet gebruiken we een beveiligde verbinding met TLS en een correct gecontroleerd certificaat. Dat valt buiten de eerste opzet van deze gids.
De PubSubClient-bibliotheek
Voor MQTT gebruiken we de bibliotheek PubSubClient van Nick O’Leary.
Open de Library Manager in de Arduino IDE en zoek naar:
PubSubClient
Installeer de bibliotheek.
PubSubClient helpt ons onder andere met:
- verbinding maken met een MQTT-broker;
- een gebruikersnaam en wachtwoord gebruiken;
- berichten publiceren;
- abonneren op topics;
- binnenkomende berichten verwerken;
- controleren of de verbinding nog actief is.
De bibliotheek neemt het MQTT-protocol voor ons uit handen. Wij blijven verantwoordelijk voor de verbinding, topicstructuur en manier waarop we op fouten reageren.
De geheime gegevens uitbreiden
Open het tabblad:
Arduino_Secrets.h
In het vorige hoofdstuk stonden daar al de WiFi-gegevens in:
#define SECRET_SSID "NaamVanJeWiFi"
#define SECRET_PASS "JeWiFiWachtwoord"
Voeg de MQTT-gegevens toe:
#define SECRET_MQTT_SERVER "192.168.1.50"
#define SECRET_MQTT_USER "esp32_sensor"
#define SECRET_MQTT_PASS "JeMQTTWachtwoord"
Het volledige bestand wordt bijvoorbeeld:
#define SECRET_SSID "WerkplaatsWiFi"
#define SECRET_PASS "JeWiFiWachtwoord"
#define SECRET_MQTT_SERVER "192.168.1.50"
#define SECRET_MQTT_USER "esp32_sensor"
#define SECRET_MQTT_PASS "JeMQTTWachtwoord"
Vervang de voorbeeldwaarden door de gegevens van jouw eigen netwerk en broker.
Plaats dit bestand niet op een openbare website en voeg het niet toe aan een openbare Git-repository.
WiFi en MQTT zijn twee afzonderlijke verbindingen
Het is belangrijk om WiFi en MQTT niet als dezelfde verbinding te behandelen.
WiFi verbindt de ESP32 met het netwerk.
MQTT verbindt de ESP32 daarna via dat netwerk met de broker.
Er kunnen daardoor verschillende toestanden ontstaan:
| WiFi | MQTT | Betekenis |
|---|---|---|
| Niet verbonden | Niet verbonden | De ESP32 heeft geen netwerkverbinding |
| Verbonden | Niet verbonden | Het netwerk werkt, maar de brokerverbinding niet |
| Verbonden | Verbonden | De ESP32 kan MQTT-berichten uitwisselen |
| Niet verbonden | Lijkt nog verbonden | Tijdelijke toestand totdat de MQTT-client het verlies verwerkt |
De MQTT-code mag alleen proberen de broker te bereiken wanneer WiFi beschikbaar is.
Geen blokkerende MQTT-lus
Op internet zie je regelmatig een MQTT-herstelroutine zoals:
while (!mqttClient.connected()) {
mqttClient.connect(...);
delay(5000);
}
Daarmee blijft de ESP32 proberen totdat de broker reageert.
Wanneer de broker uitstaat, kan de rest van het programma daardoor niet verdergaan. De sensor wordt dan mogelijk niet meer uitgelezen en lokale functies reageren niet meer.
Wij gebruiken daarom geen wachtende while-lus.
In plaats daarvan controleren we vanuit loop():
- is WiFi beschikbaar;
- is MQTT verbonden;
- is het tijd voor een nieuwe MQTT-poging?
Per controle wordt maximaal één verbindingspoging uitgevoerd. Wanneer die mislukt, gaat de ESP32 verder met zijn andere taken.
Een unieke client-ID
Iedere MQTT-client heeft een naam nodig waarmee de broker de verbinding herkent. Dit noemen we de client-ID.
Twee apparaten die tegelijk met dezelfde client-ID verbinden, kunnen elkaar van de broker verdringen. De broker denkt dan dat dezelfde client opnieuw verbinding maakt.
Daarom maken we de naam uniek met het MAC-adres van de ESP32:
mqttClientId = "MQTT-" + WiFi.macAddress();
mqttClientId.replace(":", "");
Dat levert bijvoorbeeld op:
MQTT-24D7EB123456
Het MAC-adres is een hardwarematig netwerkadres van de ESP32. Doordat dit per apparaat verschilt, kunnen meerdere ESP32’s dezelfde basiscode gebruiken zonder dezelfde MQTT-client-ID te krijgen.
Dit principe komt rechtstreeks uit de basiscode die ik voor mijn eigen ESP32-projecten gebruik.
Het status-topic
Naast de meetwaarden publiceren we de beschikbaarheid van de ESP32 op:
werkplaats/sensor/status
Wanneer de ESP32 met MQTT verbindt, publiceert hij:
online
We stellen bij het verbinden ook een Last Will and Testament in, meestal afgekort tot LWT of last will.
Daarmee vertellen we vooraf aan de broker:
Als mijn verbinding onverwacht verdwijnt, publiceer dan offline op mijn statustopic.
De broker kan dan namens de ESP32 melden dat het apparaat niet meer bereikbaar is.
Dit werkt bij een onverwachte uitval. Bij een zeer korte storing kan enige vertraging optreden voordat de broker vaststelt dat de client verdwenen is.
Retained berichten
Voor de meetwaarden en status gebruiken we retained berichten.
Een retained bericht wordt door de broker als laatste bekende waarde van een topic bewaard.
Wanneer Home Assistant zich later op het temperatuurtopic abonneert, kan de broker direct de laatst ontvangen temperatuur geven. Home Assistant hoeft dan niet te wachten totdat de ESP32 opnieuw publiceert.
Een retained meetwaarde is:
de laatst ontvangen waarde, niet noodzakelijk een meting van dit exacte moment.
Dat onderscheid wordt belangrijk wanneer een sensor lange tijd offline is. Het statustopic helpt ons om te zien of de bewaarde waarde nog door een bereikbaar apparaat wordt bijgewerkt.
Het complete programma
Onderstaande code combineert:
- de BME280;
- de herstellende WiFi-verbinding;
- een herstellende MQTT-verbinding;
- publiceren van meetwaarden;
- ontvangen van MQTT-berichten;
- een unieke MQTT-client-ID;
- een online/offline-status;
- niet-blokkerende tijdregistratie.
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
#include <WiFi.h>
#include <PubSubClient.h>
#include "Arduino_Secrets.h"
// WiFi-instellingen
const char* ssid = SECRET_SSID;
const char* password = SECRET_PASS;
const char* hostName = "werkplaats-sensor";
// MQTT-instellingen
const char* mqttBroker = SECRET_MQTT_SERVER;
const int mqttPort = 1883;
const char* mqttUser = SECRET_MQTT_USER;
const char* mqttPassword = SECRET_MQTT_PASS;
// MQTT-topics
const char* topicTemperature =
"werkplaats/sensor/temperatuur";
const char* topicHumidity =
"werkplaats/sensor/luchtvochtigheid";
const char* topicPressure =
"werkplaats/sensor/luchtdruk";
const char* topicRssi =
"werkplaats/sensor/wifi-rssi";
const char* topicStatus =
"werkplaats/sensor/status";
const char* topicCommand =
"werkplaats/sensor/opdracht";
// BME280-instellingen
Adafruit_BME280 bme;
const int sdaPin = 21;
const int sclPin = 22;
const uint8_t bmeAddress = 0x76;
// Netwerkclients
WiFiClient espClient;
PubSubClient mqttClient(espClient);
// Tijdsinstellingen
const unsigned long sensorInterval = 2000;
const unsigned long mqttPublishInterval = 10000;
const unsigned long wifiConnectTimeout = 10000;
const unsigned long wifiRetryInterval = 30000;
const unsigned long mqttRetryInterval = 10000;
// Status
bool sensorAvailable = false;
bool sensorValuesValid = false;
bool wifiWasConnected = false;
bool wifiAttemptActive = false;
// Laatst gemeten waarden
float currentTemperature = 0.0;
float currentHumidity = 0.0;
float currentPressure = 0.0;
// Tijdregistratie
unsigned long lastSensorRead = 0;
unsigned long lastMqttPublish = 0;
unsigned long wifiAttemptStarted = 0;
unsigned long lastWifiAttempt = 0;
unsigned long lastMqttAttempt = 0;
// Unieke MQTT-clientnaam
String mqttClientId;
void startWiFiConnection() {
Serial.print("Verbinden met WiFi: ");
Serial.println(ssid);
WiFi.begin(ssid, password);
wifiAttemptActive = true;
wifiAttemptStarted = millis();
lastWifiAttempt = millis();
}
void maintainWiFi() {
if (WiFi.status() == WL_CONNECTED) {
if (!wifiWasConnected) {
wifiWasConnected = true;
wifiAttemptActive = false;
Serial.println();
Serial.println("WiFi verbonden.");
Serial.print("IP-adres: ");
Serial.println(WiFi.localIP());
Serial.print("Signaalsterkte: ");
Serial.print(WiFi.RSSI());
Serial.println(" dBm");
// MQTT mag na herstel direct opnieuw proberen
lastMqttAttempt = millis() - mqttRetryInterval;
}
return;
}
if (wifiWasConnected) {
wifiWasConnected = false;
wifiAttemptActive = false;
if (mqttClient.connected()) {
mqttClient.disconnect();
}
Serial.println();
Serial.println("*** WiFi-verbinding verbroken ***");
Serial.println("De sensor blijft lokaal werken.");
startWiFiConnection();
return;
}
if (wifiAttemptActive) {
if (millis() - wifiAttemptStarted >=
wifiConnectTimeout) {
wifiAttemptActive = false;
WiFi.disconnect(false, false);
Serial.println("WiFi-verbinding niet gelukt.");
Serial.println("Het programma gaat verder.");
Serial.println(
"Over 30 seconden proberen we het opnieuw."
);
}
return;
}
if (millis() - lastWifiAttempt >=
wifiRetryInterval) {
startWiFiConnection();
}
}
void mqttCallback(
char* topic,
byte* payload,
unsigned int length
) {
String message;
for (unsigned int i = 0; i < length; i++) {
message += static_cast<char>(payload[i]);
}
message.trim();
Serial.println();
Serial.println("MQTT-bericht ontvangen");
Serial.print("Topic: ");
Serial.println(topic);
Serial.print("Bericht: ");
Serial.println(message);
if (strcmp(topic, topicCommand) == 0) {
if (message == "meting") {
Serial.println(
"Opdracht ontvangen: voer een meting uit."
);
lastSensorRead = millis() - sensorInterval;
} else {
Serial.println("Onbekende opdracht.");
}
}
}
void connectToMqtt() {
if (WiFi.status() != WL_CONNECTED) {
return;
}
Serial.print("Verbinden met MQTT als ");
Serial.print(mqttClientId);
Serial.print("... ");
bool connected = mqttClient.connect(
mqttClientId.c_str(),
mqttUser,
mqttPassword,
topicStatus,
0,
true,
"offline"
);
if (connected) {
Serial.println("gelukt.");
mqttClient.publish(
topicStatus,
"online",
true
);
if (mqttClient.subscribe(topicCommand)) {
Serial.print("Geabonneerd op: ");
Serial.println(topicCommand);
} else {
Serial.println(
"Abonneren op opdrachttopic mislukt."
);
}
// Na herstel mag direct worden gepubliceerd
lastMqttPublish =
millis() - mqttPublishInterval;
} else {
Serial.println("mislukt.");
Serial.print("MQTT-foutcode: ");
Serial.println(mqttClient.state());
Serial.println(
"De sensor blijft lokaal werken."
);
}
}
void maintainMqtt() {
if (WiFi.status() != WL_CONNECTED) {
if (mqttClient.connected()) {
mqttClient.disconnect();
}
return;
}
if (mqttClient.connected()) {
mqttClient.loop();
return;
}
if (millis() - lastMqttAttempt >=
mqttRetryInterval) {
lastMqttAttempt = millis();
connectToMqtt();
}
}
void readSensor() {
if (!sensorAvailable) {
return;
}
float temperature = bme.readTemperature();
float humidity = bme.readHumidity();
float pressure =
bme.readPressure() / 100.0F;
if (
isnan(temperature) ||
isnan(humidity) ||
isnan(pressure)
) {
sensorValuesValid = false;
Serial.println(
"Ongeldige meetwaarde ontvangen."
);
return;
}
currentTemperature = temperature;
currentHumidity = humidity;
currentPressure = pressure;
sensorValuesValid = true;
Serial.println();
Serial.println("BME280-meting");
Serial.print("Temperatuur: ");
Serial.print(currentTemperature, 1);
Serial.println(" °C");
Serial.print("Luchtvochtigheid: ");
Serial.print(currentHumidity, 1);
Serial.println(" %");
Serial.print("Luchtdruk: ");
Serial.print(currentPressure, 1);
Serial.println(" hPa");
if (WiFi.status() == WL_CONNECTED) {
Serial.println("WiFi: verbonden");
} else {
Serial.println("WiFi: niet verbonden");
}
if (mqttClient.connected()) {
Serial.println("MQTT: verbonden");
} else {
Serial.println("MQTT: niet verbonden");
}
Serial.println("--------------------");
}
bool publishValue(
const char* topic,
float value
) {
char payload[16];
snprintf(
payload,
sizeof(payload),
"%.1f",
value
);
bool published = mqttClient.publish(
topic,
payload,
true
);
Serial.print(topic);
Serial.print(" = ");
Serial.print(payload);
if (published) {
Serial.println(" gepubliceerd");
} else {
Serial.println(" publiceren mislukt");
}
return published;
}
void publishSensorValues() {
if (!mqttClient.connected()) {
return;
}
if (!sensorValuesValid) {
Serial.println(
"Geen geldige meetwaarden om te publiceren."
);
return;
}
Serial.println();
Serial.println("MQTT-waarden publiceren");
publishValue(
topicTemperature,
currentTemperature
);
publishValue(
topicHumidity,
currentHumidity
);
publishValue(
topicPressure,
currentPressure
);
publishValue(
topicRssi,
static_cast<float>(WiFi.RSSI())
);
Serial.println("--------------------");
}
void setup() {
Serial.begin(115200);
Serial.println();
Serial.println("Werkplaats-sensor starten...");
// BME280 starten
Wire.begin(sdaPin, sclPin);
if (bme.begin(bmeAddress)) {
sensorAvailable = true;
Serial.println("BME280 gevonden.");
} else {
Serial.println("BME280 niet gevonden.");
Serial.println("Het programma blijft actief.");
}
// WiFi voorbereiden
WiFi.setHostname(hostName);
WiFi.mode(WIFI_STA);
WiFi.setAutoReconnect(true);
// Unieke MQTT-client-ID maken
mqttClientId =
"MQTT-" + WiFi.macAddress();
mqttClientId.replace(":", "");
// MQTT voorbereiden
mqttClient.setServer(
mqttBroker,
mqttPort
);
mqttClient.setCallback(mqttCallback);
// Eerste pogingen direct toestaan
lastSensorRead =
millis() - sensorInterval;
lastMqttAttempt =
millis() - mqttRetryInterval;
// WiFi starten
startWiFiConnection();
}
void loop() {
maintainWiFi();
maintainMqtt();
if (millis() - lastSensorRead >=
sensorInterval) {
lastSensorRead = millis();
readSensor();
}
if (
mqttClient.connected() &&
millis() - lastMqttPublish >=
mqttPublishInterval
) {
lastMqttPublish = millis();
publishSensorValues();
}
}
De twee netwerkclients
Boven in het programma maken we twee objecten:
WiFiClient espClient;
PubSubClient mqttClient(espClient);
WiFiClient verzorgt de onderliggende netwerkverbinding via TCP.
PubSubClient gebruikt die verbinding om MQTT-berichten uit te wisselen.
Je kunt het zien als twee lagen:
WiFiClient vervoert de gegevens. PubSubClient spreekt MQTT.
Daarom moet WiFi beschikbaar zijn voordat de MQTT-client de broker kan bereiken.
De broker instellen
In setup() vertellen we de MQTT-client waar de broker bereikbaar is:
mqttClient.setServer(
mqttBroker,
mqttPort
);
De waarden komen uit:
const char* mqttBroker =
SECRET_MQTT_SERVER;
const int mqttPort = 1883;
Ook wijzen we de functie aan die binnenkomende berichten moet verwerken:
mqttClient.setCallback(mqttCallback);
Wanneer een bericht binnenkomt op een topic waarop de ESP32 is geabonneerd, roept de bibliotheek mqttCallback() aan.
MQTT alleen onderhouden wanneer WiFi werkt
De functie:
maintainMqtt()
begint met:
if (WiFi.status() != WL_CONNECTED)
Als WiFi ontbreekt, heeft een MQTT-poging geen zin.
Wanneer de MQTT-client nog denkt dat hij verbonden is, verbreken we die toestand:
mqttClient.disconnect();
Daarna eindigt de functie.
De sensorcode en WiFi-herstelcode blijven gewoon actief.
Waarom mqttClient.loop() nodig is
Wanneer MQTT verbonden is, voeren we regelmatig uit:
mqttClient.loop();
Deze opdracht helpt de bibliotheek om:
- binnenkomende berichten te verwerken;
- de verbinding actief te houden;
- keepalive-verkeer af te handelen;
- vast te stellen of de verbinding is weggevallen.
mqttClient.loop() moet daarom vaak worden aangeroepen. Plaats er geen lange, blokkerende wachttijden omheen.
De naam kan verwarrend zijn. Dit is niet dezelfde functie als de algemene Arduino-functie:
void loop()
Onze Arduino-loop() roept de MQTT-loop() steeds opnieuw aan zolang de brokerverbinding actief is.
Periodiek opnieuw verbinden
Wanneer MQTT niet verbonden is, controleren we:
if (millis() - lastMqttAttempt >=
mqttRetryInterval)
Het interval is:
const unsigned long mqttRetryInterval =
10000;
De ESP32 probeert dus iedere tien seconden eenmaal verbinding te maken.
Wanneer de poging mislukt, gaat het programma verder. Er is geen herhalende while-lus die de sensor tegenhoudt.
De volgende poging komt pas nadat het interval opnieuw is verstreken.
De MQTT-verbinding opbouwen
In:
connectToMqtt()
controleren we voor de zekerheid nogmaals of WiFi beschikbaar is.
Daarna verbinden we met:
mqttClient.connect(...)
We geven daarbij mee:
- de unieke client-ID;
- de gebruikersnaam;
- het wachtwoord;
- het statustopic;
- QoS-niveau 0 voor het last-willbericht;
- dat het last-willbericht retained moet zijn;
- de tekst offline.
Wanneer de verbinding onverwacht verdwijnt, kan de broker daardoor namens de ESP32 publiceren:
offline
op:
werkplaats/sensor/status
Online publiceren
Na een succesvolle verbinding publiceert de ESP32:
mqttClient.publish(
topicStatus,
"online",
true
);
De laatste waarde true betekent dat het bericht retained wordt opgeslagen.
Nieuwe abonnees kunnen daardoor direct zien dat de ESP32 bij zijn laatste MQTT-contact online was.
Na een onverwachte uitval vervangt de broker dit retained bericht uiteindelijk door het ingestelde last-willbericht:
offline
Opnieuw abonneren na herstel
MQTT-abonnementen uit deze eenvoudige verbinding worden niet automatisch door onze ESP32-code onthouden na een nieuwe verbinding.
Daarom voeren we na iedere succesvolle verbinding opnieuw uit:
mqttClient.subscribe(topicCommand);
Dat is belangrijk.
Wanneer de broker opnieuw start of WiFi wegvalt, moet de ESP32 na herstel opnieuw aangeven welke berichten hij wil ontvangen.
Het publiceren van meetwaarden kan na herstel direct doorgaan. Het ontvangen van opdrachten werkt pas weer nadat opnieuw is geabonneerd.
Berichten ontvangen
De callback heeft deze vorm:
void mqttCallback(
char* topic,
byte* payload,
unsigned int length
)
De bibliotheek geeft ons:
- het topic waarop het bericht binnenkwam;
- de ruwe inhoud van het bericht;
- de lengte van die inhoud.
We lopen door de ontvangen bytes en maken er een String van:
String message;
for (unsigned int i = 0; i < length; i++) {
message +=
static_cast<char>(payload[i]);
}
Daarna verwijderen we eventuele spaties en regeleinden aan het begin en einde:
message.trim();
We schrijven niet zomaar een extra teken achter de ontvangen payload. De bibliotheek garandeert namelijk niet dat daar buiten de opgegeven lengte vrije ruimte voor beschikbaar is.
Het topic controleren
Een ESP32 kan op meerdere topics geabonneerd zijn. Daarom controleren we eerst waar het bericht vandaan kwam:
if (strcmp(topic, topicCommand) == 0)
strcmp() vergelijkt twee teksten.
Een uitkomst van nul betekent dat beide teksten gelijk zijn.
Als het bericht op ons opdrachttopic binnenkomt, kijken we daarna naar de inhoud:
if (message == "meting")
Wanneer iemand meting publiceert op:
werkplaats/sensor/opdracht
zorgen we ervoor dat de volgende sensormeting direct mag worden uitgevoerd:
lastSensorRead =
millis() - sensorInterval;
De gewone meetcyclus blijft daarnaast bestaan.
Meetwaarden bewaren
De sensor wordt iedere twee seconden uitgelezen, maar we publiceren niet iedere meting.
De laatst geldige waarden bewaren we in:
float currentTemperature;
float currentHumidity;
float currentPressure;
Ook onthouden we of die waarden geldig zijn:
bool sensorValuesValid;
Hierdoor kunnen we onderscheid maken tussen:
- de sensor regelmatig uitlezen;
- de laatst bekende waarden via MQTT publiceren.
In ons voorbeeld meten we iedere twee seconden en publiceren we iedere tien seconden.
Waarom niet iedere meting publiceren?
Temperatuur en luchtvochtigheid veranderen meestal langzaam. Iedere twee seconden publiceren kan daardoor veel berichten opleveren zonder veel extra informatie.
Een rustiger publicatie-interval:
- belast de broker minder;
- beperkt netwerkverkeer;
- maakt logbestanden overzichtelijker;
- is voor omgevingsmetingen meestal voldoende.
Welk interval geschikt is, hangt af van het project.
Een alarmsignaal wil je mogelijk direct publiceren. Een kamertemperatuur hoeft misschien maar iedere minuut te worden bijgewerkt.
Een getal omzetten naar tekst
MQTT-berichten bestaan voor onze toepassing uit tekst of bytes. Onze meetwaarden staan in variabelen van het type float.
In:
publishValue()
zetten we een getal om naar tekst:
char payload[16];
snprintf(
payload,
sizeof(payload),
"%.1f",
value
);
%.1f betekent dat we één cijfer achter de komma gebruiken.
Een temperatuur van bijvoorbeeld 21.76 wordt gepubliceerd als:
21.8
Daarna versturen we de tekst met:
mqttClient.publish(
topic,
payload,
true
);
Controleren of publiceren lukt
mqttClient.publish() geeft aan of de bibliotheek het bericht kon verwerken en verzenden.
Daarom bewaren we de uitkomst:
bool published =
mqttClient.publish(...);
Vervolgens tonen we in de seriële monitor of de opdracht is gelukt.
Een succesvolle terugmelding bewijst dat de client de publicatie heeft uitgevoerd. Bij QoS 0 ontvangen we geen garantie dat iedere uiteindelijke abonnee het bericht daadwerkelijk heeft verwerkt.
De gebruikte PubSubClient-bibliotheek publiceert berichten met QoS 0. Dat is voor veel periodieke sensormetingen bruikbaar: als één meting verloren gaat, volgt later een nieuwe.
Foutcodes van PubSubClient
Wanneer een MQTT-verbinding mislukt, tonen we:
mqttClient.state()
Veelvoorkomende resultaten zijn:
| Code | Betekenis |
|---|---|
| -4 | time-out tijdens de verbinding |
| -3 | netwerkverbinding verloren |
| -2 | netwerkverbinding mislukt |
| -1 | verbinding verbroken |
| 0 | verbonden |
| 1 | broker accepteert de protocolversie niet |
| 2 | client-ID geweigerd |
| 3 | broker niet beschikbaar |
| 4 | verkeerde gebruikersnaam of verkeerd wachtwoord |
| 5 | niet geautoriseerd |
De foutcode is een aanwijzing. Combineer hem met de andere informatie.
Bij code -2 kan bijvoorbeeld sprake zijn van:
- een verkeerd brokeradres;
- een verkeerde poort;
- een firewall;
- een uitgeschakelde broker;
- een netwerkroute die ontbreekt.
Bij code 4 of 5 richten we het onderzoek eerder op de gebruikersnaam, het wachtwoord en de toegangsrechten.
Het programma uploaden
Controleer vóór het uploaden:
- de PubSubClient-bibliotheek is geïnstalleerd;
- het brokeradres klopt;
- poort 1883 is correct;
- de MQTT-gebruikersnaam bestaat;
- het MQTT-wachtwoord klopt;
- de broker is gestart;
- de ESP32 en broker kunnen elkaar via het netwerk bereiken;
- de BME280 is nog correct aangesloten;
- het juiste bord en de juiste USB-poort zijn geselecteerd.
Upload daarna het programma en open de seriële monitor op:
115200 baud
Bij een succesvolle verbinding kan de uitvoer er ongeveer zo uitzien:
Werkplaats-sensor starten...
BME280 gevonden.
Verbinden met WiFi: WerkplaatsWiFi
WiFi verbonden.
IP-adres: 192.168.1.74
Signaalsterkte: -52 dBm
Verbinden met MQTT als MQTT-24D7EB123456...
gelukt.
Geabonneerd op: werkplaats/sensor/opdracht
Daarna verschijnen de metingen en publicaties.
De topics controleren
Gebruik een MQTT-client om de berichten te bekijken. Dat kan bijvoorbeeld via:
- MQTT Explorer;
- de luisterfunctie binnen Home Assistant;
- een tweede programma dat op MQTT is aangesloten;
- de commandoregelprogramma’s van Mosquitto.
Abonneer je voor een brede test op:
werkplaats/sensor/#
Het hekje is een wildcard. Daarmee ontvang je alle onderliggende topics onder:
werkplaats/sensor/
Je kunt dan berichten zien op:
werkplaats/sensor/temperatuur
werkplaats/sensor/luchtvochtigheid
werkplaats/sensor/luchtdruk
werkplaats/sensor/wifi-rssi
werkplaats/sensor/status
Gebruik zo’n brede wildcard vooral voor testen. In een definitieve toepassing abonneren we ons bij voorkeur alleen op de topics die werkelijk nodig zijn.
Een opdracht versturen
Publiceer met een andere MQTT-client het bericht:
meting
op:
werkplaats/sensor/opdracht
De seriële monitor moet dan tonen:
MQTT-bericht ontvangen
Topic: werkplaats/sensor/opdracht
Bericht: meting
Opdracht ontvangen: voer een meting uit.
Daarna wordt de sensor bij de volgende uitvoering van loop() direct opnieuw uitgelezen.
Publiceer je een andere tekst, dan verschijnt:
Onbekende opdracht.
De ESP32 blijft gewoon werken.
Test 1 — Normale start
Start de ESP32 terwijl WiFi en de broker beschikbaar zijn.
Controleer:
- Wordt de BME280 gevonden?
- Komt WiFi tot stand?
- Verschijnt een IP-adres?
- Maakt de ESP32 verbinding met MQTT?
- Wordt opnieuw geabonneerd op het opdrachttopic?
- Verschijnen de meetwaarden in de seriële monitor?
- Komen de waarden aan bij de broker?
- Staat het statustopic op online?
Zo testen we de volledige route:
Sensor → ESP32 → WiFi → broker → MQTT-client
Test 2 — Broker uitgeschakeld
Laat WiFi actief, maar schakel de MQTT-broker tijdelijk uit.
De ESP32 moet:
- verbonden blijven met WiFi;
- de BME280 blijven uitlezen;
- de meetwaarden in de seriële monitor blijven tonen;
- aangeven dat MQTT niet verbonden is;
- iedere tien seconden opnieuw proberen;
- foutcodes tonen zonder opnieuw op te starten.
Schakel de broker daarna weer in.
Bij een volgende poging moet de ESP32:
- opnieuw verbinden;
- online publiceren;
- zich opnieuw abonneren;
- de laatst gemeten waarden weer publiceren.
Er is geen reset van de ESP32 nodig.
Test 3 — WiFi uitgeschakeld
Schakel daarna tijdelijk het accesspoint uit.
De ESP32 moet:
- de WiFi-uitval detecteren;
- de MQTT-verbinding loslaten;
- de sensor blijven uitlezen;
- periodiek WiFi blijven proberen;
- geen nutteloze MQTT-pogingen doen zolang WiFi ontbreekt.
Schakel het accesspoint opnieuw in.
De herstelvolgorde hoort te zijn:
- WiFi wordt opnieuw opgebouwd.
- De ESP32 ontvangt een IP-adres.
- MQTT probeert opnieuw te verbinden.
- De ESP32 publiceert online.
- De ESP32 abonneert zich opnieuw.
- De meetwaarden worden weer gepubliceerd.
Deze volgorde helpt bij het foutzoeken. MQTT kan immers pas herstellen nadat WiFi is teruggekeerd.
Test 4 — Verkeerde MQTT-gegevens
Vul tijdelijk een verkeerde MQTT-gebruikersnaam of een verkeerd wachtwoord in en upload de code.
WiFi moet nog steeds verbinden en de sensor moet blijven meten.
De broker zal de MQTT-aanmelding waarschijnlijk weigeren. Bekijk de foutcode in de seriële monitor.
Herstel daarna de juiste gegevens en upload de code opnieuw.
Hiermee tonen we aan dat een authenticatiefout bij MQTT een ander probleem is dan een ontbrekende WiFi-verbinding.
Test 5 — Dubbele client-ID
Onze code gebruikt het MAC-adres om een unieke client-ID te maken. Daardoor kunnen meerdere ESP32’s dezelfde basiscode gebruiken.
Als twee clients toch dezelfde vaste ID gebruiken, kan het volgende gebeuren:
- client A maakt verbinding;
- client B maakt verbinding met dezelfde ID;
- de broker verbreekt de verbinding met client A;
- client A probeert opnieuw;
- de broker verbreekt mogelijk client B;
- beide apparaten blijven elkaar afwisselend verdringen.
Dat kan eruitzien als een instabiele broker- of WiFi-verbinding, terwijl de werkelijke oorzaak een dubbele client-ID is.
Een unieke naam is dus geen cosmetisch detail.
Wanneer MQTT niet verbindt
Controleer de keten in een vaste volgorde.
Werkt de sensor?
Verschijnen er geldige meetwaarden in de seriële monitor?
Zo ja, dan werkt het lokale sensorgedeelte.
Werkt WiFi?
Verschijnt:
WiFi verbonden
en krijgt de ESP32 een geldig IP-adres?
Zo nee, los eerst de WiFi-verbinding op.
Is de broker bereikbaar?
Controleer:
- het IP-adres;
- de poort;
- of de broker actief is;
- of een firewall de verbinding blokkeert;
- of de ESP32 en broker in netwerken zitten die elkaar mogen bereiken.
Kloppen de aanmeldgegevens?
Controleer:
- hoofdletters;
- bijzondere tekens;
- onbedoelde spaties;
- rechten van de MQTT-gebruiker;
- toegestane topics.
Kan een andere client verbinden?
Probeer met MQTT Explorer of een andere MQTT-client vanaf hetzelfde lokale netwerk verbinding te maken met exact dezelfde brokergegevens.
Als ook die client niet kan verbinden, ligt het probleem waarschijnlijk niet in de ESP32-code.
Wanneer verbinden lukt maar geen waarden verschijnen
Controleer dan:
- gebruik je exact het juiste topic;
- is de hoofdlettergebruik gelijk;
- publiceert de ESP32 volgens de seriële monitor;
- is sensorValuesValid waar;
- is de MQTT-client nog verbonden;
- is de abonnement-wildcard correct;
- zijn er toegangsregels op de broker;
- is het bericht misschien retained onder een ander topic opgeslagen?
Luister voor een eerste test op:
werkplaats/sensor/#
Als daar wel berichten verschijnen, maar jouw specifieke abonnement niets ontvangt, zit het probleem waarschijnlijk in de gebruikte topicnaam.
Wanneer opdrachten niet aankomen
Als publiceren vanaf de ESP32 wel werkt, maar opdrachten niet worden ontvangen, controleer dan:
- is na de verbinding Geabonneerd op verschenen;
- publiceer je op werkplaats/sensor/opdracht;
- komt de schrijfwijze exact overeen;
- wordt mqttClient.loop() regelmatig uitgevoerd;
- blokkeert andere code de hoofdloop niet;
- heeft de MQTT-gebruiker rechten om op dit topic te abonneren;
- blijft MQTT werkelijk verbonden?
Een client kan alleen binnenkomende berichten verwerken wanneer mqttClient.loop() voldoende vaak wordt aangeroepen.
MQTT is geen opslagdatabase
De broker verdeelt berichten. Hij is niet automatisch een volledige historische database.
Retained berichten bewaren alleen de laatste retained waarde per topic. Ze vormen geen complete meetgeschiedenis.
Wil je temperatuurwaarden over dagen of maanden bekijken, dan heb je een systeem nodig dat de MQTT-berichten ontvangt en opslaat, bijvoorbeeld:
- Home Assistant met zijn recorder;
- InfluxDB;
- een andere tijdreeksdatabase;
- eigen software die berichten opslaat.
MQTT verzorgt het transport. Een andere toepassing verzorgt de langdurige opslag en presentatie.
Voorbereiding op Home Assistant
Onze topics kunnen later in Home Assistant als sensoren worden toegevoegd.
De belangrijkste koppelingen zijn dan:
| Gegeven | MQTT-topic |
|---|---|
| Temperatuur | werkplaats/sensor/temperatuur |
| Luchtvochtigheid | werkplaats/sensor/luchtvochtigheid |
| Luchtdruk | werkplaats/sensor/luchtdruk |
| WiFi-signaal | werkplaats/sensor/wifi-rssi |
| Beschikbaarheid | werkplaats/sensor/status |
Home Assistant kan het statustopic gebruiken om te bepalen of het apparaat beschikbaar is:
- online betekent beschikbaar;
- offline betekent niet beschikbaar.
In een later project kunnen we MQTT Discovery toevoegen. Daarmee kan de ESP32 zijn sensoren automatisch bij Home Assistant aanmelden. Voor dit hoofdstuk houden we de topics bewust zichtbaar en handmatig controleerbaar.
Beveiliging
De voorbeeldcode gebruikt MQTT op poort 1883 zonder TLS.
Dat is geschikt voor een eerste project binnen een vertrouwd en afgeschermd lokaal netwerk, maar niet voor het onbeveiligd aanbieden van de broker op internet.
Houd rekening met het volgende:
- stel poort 1883 niet zomaar open naar internet;
- gebruik een afzonderlijke MQTT-gebruiker;
- geef die gebruiker alleen de noodzakelijke rechten;
- gebruik een sterk wachtwoord;
- plaats geheime gegevens niet in openbare code;
- gebruik voor externe verbindingen TLS en certificaatcontrole;
- vertrouw niet alleen op een moeilijk te raden topicnaam.
Een gebruikersnaam en wachtwoord zonder TLS kunnen bij onderschept netwerkverkeer zichtbaar worden.
Wat je uit dit hoofdstuk kunt meenemen
We hebben de lokale sensor uitgebreid tot een netwerkapparaat dat gegevens kan publiceren en opdrachten kan ontvangen.
Daarbij heb je ontdekt dat:
- MQTT berichten via een centrale broker uitwisselt;
- een publisher berichten op topics publiceert;
- een subscriber zich op topics abonneert;
- topics hoofdlettergevoelig zijn;
- WiFi en MQTT twee afzonderlijke verbindingen zijn;
- MQTT alleen kan werken wanneer het netwerk beschikbaar is;
- de PubSubClient-bibliotheek de MQTT-communicatie verzorgt;
- mqttClient.loop() regelmatig moet worden uitgevoerd;
- een MQTT-herstelpoging de rest van het programma niet hoeft te blokkeren;
- de ESP32 periodiek opnieuw kan verbinden;
- na herstel opnieuw op topics moet worden geabonneerd;
- een uniek client-ID voorkomt dat apparaten elkaar van de broker verdringen;
- het MAC-adres kan worden gebruikt om zo’n unieke naam te maken;
- retained berichten de laatst gepubliceerde waarde bewaren;
- een last-willbericht een onverwachte uitval zichtbaar maakt;
- de sensor blijft meten wanneer de broker ontbreekt;
- meetwaarden alleen worden gepubliceerd wanneer ze geldig zijn;
- MQTT-foutcodes helpen om het probleem af te bakenen;
- MQTT berichten vervoert, maar niet automatisch een volledige meetgeschiedenis opslaat;
- poort 1883 geen versleutelde verbinding biedt.
De belangrijkste foutzoekvraag wordt vanaf nu:
Welk deel van de keten werkt nog?
We onderzoeken afzonderlijk:
Sensor → WiFi → MQTT-client → broker → topic → ontvanger
Wanneer je precies kunt aangeven tot welk punt de gegevens komen, wordt een ogenschijnlijk groot netwerkprobleem een veel kleinere en beter testbare vraag.
Technische naslag
Dit hoofdstuk bouwt voort op mijn failsafe ESP32-basis voor WiFi, MQTT en OTA, met afzonderlijke herstelintervallen en zonder OTA. De gebruikte MQTT-client en de actuele beperkingen daarvan staan in de officiële PubSubClient-documentatie en PubSubClient API.