Send DHT11 readings to a live cloud dashboard, and switch an LED or relay back from anywhere.
Project_11_ESP32_MQTT_Ubidots.ino, then open it in the Arduino IDE. Needs the Ubidots ESP32 MQTT, DHT sensor library, and Adafruit Unified Sensor libraries, plus your Wi-Fi and Ubidots token.
By the end of this project, you will:
loop(), and recover from a drop without rebooting.In Projects 5 to 9 the ESP32 was a web server: your phone had to be on the same Wi-Fi and had to keep asking for data. MQTT flips that around. Devices publish messages to a broker, and anyone interested subscribes. It is lightweight and works from anywhere with internet.
| Term | Meaning | Here |
|---|---|---|
| Broker | Server that routes all messages | Hosted by Ubidots |
| Publish | Send a value to a topic | ESP32 sends temperature, humidity |
| Subscribe | Ask to receive a topic | ESP32 listens for led |
stem.ubidots.com.BBUS-xxxx).Install from the Library Manager: Ubidots ESP32 MQTT (pulls in PubSubClient), DHT sensor library, Adafruit Unified Sensor.
| Qty | Part | Notes |
|---|---|---|
| 1 | ESP32 · breadboard · jumper wires | as always |
| 1 | DHT11 module | S to GPIO 4, + to 3.3 V, - to GND (same as Project 9) |
| 1 | LED + 220 Ohm, or the relay from Project 7 | to GPIO 26 (the thing the cloud switches) |
Edit the marked block at the top of the sketch: WIFI_SSID, WIFI_PASS, UBIDOTS_TOKEN, and a unique DEVICE_LABEL. Upload, then open Serial Monitor at 115200:
============================================== Project 11: ESP32 + MQTT + Ubidots ============================================== DHT11 data -> GPIO 4 Output -> GPIO 26 [PUB] temperature: 24.6 C, humidity: 41.0 % [CLOUD] led -> ON (raw "1")
The first sketch that talks both ways with something outside the room. Here is how it works.
const char *WIFI_SSID = "REPLACE_WITH_YOUR_SSID"; const char *WIFI_PASS = "REPLACE_WITH_YOUR_PASSWORD"; const char *UBIDOTS_TOKEN = "REPLACE_WITH_YOUR_UBIDOTS_TOKEN"; const char *DEVICE_LABEL = "esp32-workshop"; // a name for THIS board const char *VAR_TEMPERATURE = "temperature"; const char *VAR_HUMIDITY = "humidity"; const char *VAR_LED = "led"; // the control variable we subscribe to const int PUBLISH_EVERY_MS = 5000; // send readings every 5 seconds
Everything that would change if you moved to another MQTT platform sits in one place. The rest of the sketch reads the same whether the broker is Ubidots, Adafruit IO, or one you run yourself.
DEVICE_LABEL is the name your board files data under, so every student needs a different one.DHT dht(DHTPIN, DHTTYPE); Ubidots ubidots(UBIDOTS_TOKEN); unsigned long lastPublish = 0;
dht is Project 9's sensor object, unchanged. ubidots wraps a Wi-Fi client, an MQTT client (PubSubClient underneath), and Ubidots' topic naming. Handing it the token at construction is what ties this board to your account.
callback(), the cloud talking backvoid callback(char *topic, byte *payload, unsigned int length) { char value[8] = {0}; unsigned int n = (length < sizeof(value) - 1) ? length : sizeof(value) - 1; memcpy(value, payload, n); float v = atof(value); bool on = (v >= 0.5); // treat anything from ~1 as "on" digitalWrite(LED_PIN, on ? HIGH : LOW); Serial.printf("[CLOUD] led -> %s (raw \"%s\")\n", on ? "ON" : "OFF", value); }
This function is never called by your code. You hand it to the library, and the library calls it whenever a subscribed value arrives. That inversion has a name, callback, and it is how almost all event-driven networking works.
The payload arrives as raw bytes with a length, not a C string, so it has no terminating zero. Copying at most 7 bytes into a zeroed 8-byte buffer guarantees one, which is what makes atof() safe. Without the n guard, a long payload would write past the end of value.
v >= 0.5 rather than v == 1 is defensive: a switch may send 1, 1.0, or 1.000000, and comparing floats for exact equality is a bad habit anyway.
setup() connects, then subscribesubidots.setDebug(true); // prints connection details ubidots.connectToWifi(WIFI_SSID, WIFI_PASS); ubidots.setCallback(callback); ubidots.setup(); ubidots.reconnect(); ubidots.subscribeLastValue(DEVICE_LABEL, VAR_LED);
The order matters. Wi-Fi first, because MQTT needs a network. Then register the callback, so no message can arrive before there is somewhere to deliver it. Then connect to the broker, and only then subscribe.
subscribeLastValue() also asks the broker for the variable's current value, so the board picks up a switch you left on before it booted.
loop() keeps the link alive and publishes on a timerif (!ubidots.connected()) { ubidots.reconnect(); ubidots.subscribeLastValue(DEVICE_LABEL, VAR_LED); } if (millis() - lastPublish > PUBLISH_EVERY_MS) { float t = dht.readTemperature(); float h = dht.readHumidity(); if (isnan(t) || isnan(h)) { Serial.println("[DHT] Failed to read, skipping this publish"); } else { ubidots.add(VAR_TEMPERATURE, t); ubidots.add(VAR_HUMIDITY, h); ubidots.publish(DEVICE_LABEL); } lastPublish = millis(); } ubidots.loop(); // lets the library process incoming cloud messages
Three jobs, none of them blocking:
millis() timer. Two add() calls then one publish() sends both readings in a single message.ubidots.loop() is where incoming messages are read off the socket and your callback() gets invoked. Miss it and nothing arrives.Note the isnan() check again: a failed reading skips the publish entirely. Sending garbage to a chart is worse than sending nothing, because the gap is honest and the spike is not.
delay() had to go. A blocking wait here would stall ubidots.loop(), and cloud commands would arrive late or not at all.| Code | What it does |
|---|---|
Ubidots ubidots(TOKEN) | Creates the client: Wi-Fi, MQTT, topic naming, and your credentials in one object. |
ubidots.connectToWifi(...) | Joins the network, the same job WiFi.begin() did in Project 5. |
ubidots.setCallback(callback) | Registers the function the library calls when a subscribed value arrives. |
ubidots.subscribeLastValue(DEVICE_LABEL, VAR_LED) | Subscribes to led and pulls its current value immediately. |
ubidots.add(VAR, value) | Queues one reading. Several add() calls travel in one message. |
ubidots.publish(DEVICE_LABEL) | Sends the queued readings to the broker, filed under this device. |
ubidots.loop() | Services the MQTT connection. Incoming messages are delivered from here. |
ubidots.connected() | Reports whether the broker link is still up, so loop() can heal it. |
millis() - lastPublish > PUBLISH_EVERY_MS | Project 4's non-blocking timer, now pacing network traffic. |
isnan(t) || isnan(h) | Skips the publish on a bad reading, leaving a gap rather than a lie. |
memcpy into a zeroed buffer | Turns a raw MQTT payload into a proper C string before atof(). |
temperature and humidity.led.led.This is the project where "connected device" stops meaning "same Wi-Fi as my phone". The key takeaways:
led command). A device that only reports is a sensor; one that also listens is an IoT device.connected() and re-subscribing costs four lines and is the difference between a demo and something that survives a lunch break.ubidots.loop() has to run constantly.| Symptom | Likely cause | Fix |
|---|---|---|
| Serial stops after Wi-Fi | Wrong token | Re-copy the Default token |
| No device in Ubidots | Never published | Check DHT reads numbers, not -- |
| Switch does nothing | Variable not named led | Name it exactly led |
| Reconnect loop | 5 GHz Wi-Fi | ESP32 is 2.4 GHz only |