Sending Sensor Data with MQTT
| Difficulty | Advanced |
|---|---|
| Category | Communication |
| Supported boards | ESP32 DevKit, 30-pin |
| Time needed | 75 minutes |
| Prerequisites | Serving a Page from an ESP32 |
| Tested board | Not yet verified |
| Tested on | Not yet verified |
| Arduino IDE version | Not yet verified |
| Library | Not yet verified |
What it does
The web server tutorial made the board wait for someone to ask. MQTT turns that round: the board publishes a reading whenever it has one, and anything interested subscribes and receives it.
That change of direction fixes the three things the web server could not do. Several things can watch the same sensor at once. The board can be behind a router with no port forwarding, because it makes the connection outward. And a dashboard, a phone app and a logging script can all consume the same data without the board knowing any of them exist.
It is how almost every real sensor network is built, and it is less complicated than it sounds.
Before you begin
Required parts and tools
- One ESP32 board, already connected to your Wi-Fi.
- A sensor. A DHT22 on
GPIO 4is assumed below; anything works. - A broker. See the next section; this is the one new piece.
- An MQTT client on your phone or computer, to watch what arrives.
The vocabulary, in four lines
- Broker: the server in the middle. Everything connects to it; nothing connects to anything else.
- Topic: a label like
siratek/greenhouse/temperature. Slashes make a hierarchy. - Publish: send a value to a topic.
- Subscribe: ask to receive everything sent to a topic.
Publishers and subscribers never meet. That is the whole design.
Choosing a broker
Three options, in increasing order of effort and decreasing order of risk.
A public test broker. test.mosquitto.org and similar exist for exactly this. They need no account and work immediately.
They are also completely public. Anything you publish can be read by anyone, and anyone can publish to your topics. Use one to learn, with meaningless data, and use an unusual topic prefix so you are not confused by other people’s traffic. Never put anything real on one.
Your own broker. Mosquitto on a Raspberry Pi or any always-on computer on your network. It stays on your network, you control the accounts, and it is a package install plus a config file. This is the right answer for a home or workshop.
A hosted broker. Several offer a free tier with TLS and accounts. Right when the data must reach you from somewhere else, and the point at which you should be using a username and password.
This page uses a public broker so it works today, and says plainly where that stops being appropriate.
Topic names
Use a hierarchy, lowercase, no spaces:
siratek/greenhouse/temperature
siratek/greenhouse/humidity
siratek/greenhouse/status
Subscribers can use wildcards: siratek/greenhouse/+ gets all three, siratek/# gets everything below siratek.
Put something unique at the front on a public broker. siratek is a poor choice precisely because this tutorial published it; use something nobody else would type.
Quality of service, briefly
MQTT offers three delivery guarantees. For a sensor publishing every thirty seconds, QoS 0 is correct: if a reading is lost, another arrives shortly. Higher levels cost memory and complexity to solve a problem a temperature sensor does not have.
The library below uses 0 by default, which is the right default.
Pin reference
| Pin | Purpose | Notes |
|---|---|---|
GPIO 4 |
DHT22 data | Any general-purpose pin |
3V3, GND |
Sensor power | The ESP32 is 3.3 V throughout |
Wiring steps
- Unplug the USB cable.
- DHT22
+to3V3,-toGND,outtoGPIO 4. - Plug the USB cable back in.
Software and libraries
Install PubSubClient by Nick O’Leary through Library Manager, plus the DHT sensor library and Adafruit Unified Sensor if you are using the DHT22.
PubSubClient has one setting that catches everyone, covered below.
Minimal working example
// Siratek: publish a temperature reading over MQTT.
#include <WiFi.h>
#include <PubSubClient.h>
#include <DHT.h>
const char* ssid = "your network name";
const char* password = "your password";
// A public broker. Fine for learning, not for anything real.
const char* brokerHost = "test.mosquitto.org";
const int brokerPort = 1883;
// Change this prefix to something nobody else would use.
const char* topicTemperature = "siratek/greenhouse/temperature";
const char* topicStatus = "siratek/greenhouse/status";
DHT dht(4, DHT22);
WiFiClient wifi;
PubSubClient mqtt(wifi);
char clientId[32];
void connectWiFi() {
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
unsigned long start = millis();
while (WiFi.status() != WL_CONNECTED && millis() - start < 20000) {
delay(500);
}
}
bool connectBroker() {
if (mqtt.connected()) {
return true;
}
Serial.print("connecting to broker... ");
// The last argument set is a "will": the broker sends this if we vanish.
if (mqtt.connect(clientId, NULL, NULL, topicStatus, 0, true, "offline")) {
Serial.println("connected");
mqtt.publish(topicStatus, "online", true);
return true;
}
Serial.print("failed, state ");
Serial.println(mqtt.state());
return false;
}
void setup() {
Serial.begin(115200);
dht.begin();
// A client id must be unique on the broker. Build one from the MAC.
uint64_t mac = ESP.getEfuseMac();
snprintf(clientId, sizeof(clientId), "siratek-%04X%08lX",
(unsigned int)(mac >> 32), (unsigned long) mac);
Serial.print("Siratek: client id ");
Serial.println(clientId);
connectWiFi();
mqtt.setServer(brokerHost, brokerPort);
}
unsigned long lastPublish = 0;
unsigned long lastAttempt = 0;
void loop() {
if (WiFi.status() != WL_CONNECTED) {
connectWiFi();
return;
}
// Retry the broker every five seconds, not every pass.
if (!mqtt.connected()) {
if (millis() - lastAttempt < 5000) {
return;
}
lastAttempt = millis();
connectBroker();
return;
}
mqtt.loop(); // must be called often
if (millis() - lastPublish >= 30000) {
lastPublish = millis();
float t = dht.readTemperature();
if (isnan(t)) {
Serial.println("sensor read failed, not publishing");
return;
}
char payload[16];
snprintf(payload, sizeof(payload), "%.1f", t);
if (mqtt.publish(topicTemperature, payload)) {
Serial.print("published ");
Serial.println(payload);
} else {
Serial.println("publish failed");
}
}
}
Four things in there are worth more than the rest.
The client id is unique. Brokers allow one connection per id, so two boards with the same id kick each other off repeatedly. Building it from the chip’s MAC address guarantees uniqueness without you having to remember.
The “will” message. Those extra arguments to connect() tell the broker: if this client disappears without saying goodbye, publish offline to the status topic. So a subscriber can tell “the sensor says it is 24 degrees” from “the sensor has been gone for an hour”, which a plain publish cannot. It is the single most useful MQTT feature for an unattended node.
Reconnection is rate-limited. Trying every pass of loop() hammers the broker and makes the serial output useless. Five seconds is plenty.
mqtt.loop() must be called often. It handles keepalives and incoming messages. A delay() anywhere in loop() eventually drops the connection.
The payload size limit
PubSubClient’s default buffer is small, historically 128 bytes including the topic and headers. A longer JSON payload is silently dropped: publish() returns false and nothing arrives.
If you publish anything beyond a single number:
mqtt.setBufferSize(512);
This is the single most common reason “my publish does nothing”, and nothing in the output points at it.
Receiving commands
The same connection works both ways, which is how you send something back to the board.
void onMessage(char* topic, byte* payload, unsigned int length) {
// payload is NOT null-terminated. Copy it before treating it as a string.
char message[32];
unsigned int n = min(length, (unsigned int)(sizeof(message) - 1));
memcpy(message, payload, n);
message[n] = '\0';
Serial.print("received on ");
Serial.print(topic);
Serial.print(": ");
Serial.println(message);
if (strcmp(message, "on") == 0) {
digitalWrite(ledPin, HIGH);
} else if (strcmp(message, "off") == 0) {
digitalWrite(ledPin, LOW);
}
}
Register it with mqtt.setCallback(onMessage) in setup(), and subscribe after connecting:
mqtt.subscribe("siratek/greenhouse/command");
The payload is not a string. It is a byte array with a length, and reading past it gives you whatever happened to be in memory. Copying it with a bounded length, as above, is not caution; it is required.
Subscribe after every reconnection, not once in setup(). Subscriptions do not survive a dropped connection unless the broker is holding a session for you.
Expected result
A client id and a connection message on the serial monitor, then a published value every thirty seconds.
Open an MQTT client, connect to the same broker, and subscribe to siratek/greenhouse/#. You should see the readings arrive, and online on the status topic.
Then unplug the board. Within the keepalive interval the broker should publish offline to the status topic on its behalf. Watching that happen is the moment MQTT makes sense.
Troubleshooting
| Symptom | Likely cause | Check or fix |
|---|---|---|
publish() returns false |
Payload over the buffer size | setBufferSize(512) |
| Connects, then drops repeatedly | Two clients with the same id | Build the id from the MAC |
| Never connects, state -2 | Broker unreachable | Check the hostname and the network |
| Connects, no messages arrive | Subscribed to a different topic | Topics are case sensitive |
| Messages stop after a while | mqtt.loop() not called often |
Remove any delay() from loop() |
| Subscriptions lost after a reconnect | Not resubscribed | Subscribe inside the reconnect |
| Garbage at the end of a received message | Payload treated as a string | Copy it with its length |
| Other people’s data on your topic | Public broker | Use a unique prefix; run your own |
| Nothing knows the sensor died | No will message | Set one on connect() |
| Works on the bench, not in the shed | Wi-Fi signal | Check RSSI; add an antenna or move it |
Next steps
- Easier first: Serving a Page from an ESP32, which this page is the answer to.
- Related: Making a Project Survive Unattended, which every unattended node needs.
- Next project: a node per room, each publishing to its own topic, with one dashboard subscribing to all of them.
Sources and attribution
All text, wiring guidance and code in this tutorial are original Siratek work.
- MQTT’s publish and subscribe model, topics, wildcards, quality of service levels and last will are defined by the MQTT specification and described here in the plainest terms.
- PubSubClient documentation for the calls used and the default buffer size.
- Arduino-ESP32 documentation for
WiFiandESP.getEfuseMac().
The caution about public test brokers is a statement of how they work rather than a criticism of them. No third-party tutorial text, diagrams, photographs or code has been reproduced here.

