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.
diagram.json above and paste its wiring in to load the circuit.
🎯 Objective
By the end of this project, you will:
- Explain the difference between the request/response model of a web server and the publish/subscribe model of MQTT, and why the second one scales.
- Use the vocabulary every MQTT platform shares: broker, topic, publish, subscribe, QoS.
- Send real sensor readings to a cloud service and see them plotted over time.
- Receive a command from the cloud through a callback function, and act on it.
- Keep a network connection alive in
loop(), and recover from a drop without rebooting. - Build a dashboard someone else can use, from anywhere, with no code on their side.
1Why MQTT
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 |
| QoS | How hard the broker guarantees delivery (0, 1, or 2) | QoS 0, see below |
MQTT actually defines three Quality of Service levels: QoS 0 fires a message and does not wait for confirmation, QoS 1 guarantees it arrives at least once (a duplicate is possible), and QoS 2 guarantees exactly once at the cost of extra round trips. PubSubClient, the library Ubidots ESP32 MQTT is built on, can only publish at QoS 0, so every ubidots.publish() call in this sketch is QoS 0. That is the right trade for a sensor reading: a lost temperature publish is replaced by the next one 5 seconds later, cheaper than the extra network round trips QoS 1 or 2 would add.
2Get your Ubidots token
- Sign up for a free Ubidots STEM account at
stem.ubidots.com. - Open your profile, then API Credentials, and copy the Default token (looks like
BBUS-xxxx). - No need to create the device by hand: it appears the first time the board publishes.
3Install the required libraries
From the Arduino Library Manager (setup: Foundation 3):
- Ubidots ESP32 MQTT (this also installs PubSubClient)
- DHT sensor library (Adafruit)
- Adafruit Unified Sensor (dependency of the DHT library)
4Parts and wiring
| 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) |
5Set your details and upload
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")
6Code Walkthrough: Understanding the Sketch
The first sketch that talks both ways with something outside the room. Here is how it works.
Step 1: One block holds everything platform-specific
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.Step 2: Two objects, two responsibilities
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.
Step 3: callback(), the cloud talking back
void 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.
Step 4: setup() connects, then subscribes
ubidots.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.
Step 5: loop() keeps the link alive and publishes on a timer
if (!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); Serial.printf("[PUB] %s: %.1f C, %s: %.1f %%\n", VAR_TEMPERATURE, t, VAR_HUMIDITY, h); } lastPublish = millis(); } ubidots.loop(); // lets the library process incoming cloud messages
Three jobs, none of them blocking:
- Heal the connection. Wi-Fi drops and brokers restart. Re-subscribing after a reconnect is easy to forget, and the symptom (data still uploads, the switch stops working) is confusing.
- Publish on a schedule, using Project 4's
millis()timer. Twoadd()calls then onepublish()sends both readings in a single message. - Give the library time to work.
ubidots.loop()is where incoming messages are read off the socket and yourcallback()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.Key concepts: what each line really does
| 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(). |
7Build the dashboard
- In Devices, confirm your device appeared with
temperatureandhumidity. - Add a control variable: open the device, Add Variable, type Raw, name it exactly
led. - In Data, Dashboards, add a Gauge on temperature, a Line chart on temperature and humidity, and a Switch on
led. - Flip the switch. The board's LED or relay follows within a few seconds.
🎯 Knowledge Check
1. How does MQTT differ from the web-server model?
2. What is an MQTT "broker"?
3. Why must you re-subscribe after a Wi-Fi reconnect?
4. What does the callback() function do in an MQTT sketch?
5. Why should you check isnan() before publishing a DHT11 reading?
9Wrapping Up: What You've Learned
This is the project where "connected device" stops meaning "same Wi-Fi as my phone". The key takeaways:
- Publish/subscribe decouples everyone. The board does not know who reads its data and the dashboard does not know where the board is. A broker in the middle is what makes that work from anywhere.
- The loop is now closed: device to cloud (readings) and cloud to device (the
ledcommand). A device that only reports is a sensor; one that also listens is an IoT device. - Callbacks invert control. You write the handler, the library decides when to run it. Keep it short and never block inside it.
- Network code must self-heal. Checking
connected()and re-subscribing costs four lines and is the difference between a demo and something that survives a lunch break. - Non-blocking timing is now mandatory, not a style preference:
ubidots.loop()has to run constantly. - Tokens are passwords. They stay in the edit block, out of screenshots, and out of git.
- Bad data is worse than no data. Skipping a publish leaves an honest gap in the chart.
Now try
- Publish humidity too: Add a second Ubidots variable for humidity and publish both temperature and humidity as separate variables.
- Cloud-controlled relay: Wire an LED to GPIO 26 and add a Ubidots switch widget that turns it on/off remotely from the dashboard.
10Troubleshooting
| 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 |
11Going further
- Swap the LED for the relay (Project 7) and you are switching mains-style loads from the cloud.
- Add an event in Ubidots (Data, then Events) to email or notify you when temperature crosses a threshold.
- Retained messages. A retained message is kept by the broker and handed to any new subscriber immediately, even if they connect after it was sent.
subscribeLastValue()in this sketch already gets you a version of that effect for theledcommand (Ubidots remembers the last value and hands it over on subscribe); a raw MQTTpublish(topic, payload, true)(the trailingtrueis the retain flag) does the same thing at the protocol level, so a new subscriber to a topic, including your own board after a reboot, learns the last known value without asking. - Last-Will-and-Testament (LWT). An LWT is a message you register when you first connect that the broker sends on your behalf if your connection drops without a clean disconnect. It's how other subscribers learn a device went offline instead of just falling silent. The library underneath this sketch, PubSubClient, takes optional will-topic, will-QoS, will-retain, and will-message arguments in its
connect()call for exactly this purpose. Ubidots' wrapper manages the connection for you and doesn't expose those arguments directly, but if you moved to a rawPubSubClient(or another MQTT platform), this is the call you'd reach for to publish an honest "offline" status the moment a device disappears. - Reconnect backoff.
loop()above retriesubidots.reconnect()on every single pass while disconnected, which can hammer a struggling broker with attempts. A small backoff fixes that: wait longer between retries each time, and reset once the connection is back:unsigned long lastReconnectAttempt = 0; unsigned long reconnectBackoff = 2000; // start at 2 s // inside loop(), in place of the plain reconnect check: if (ubidots.connected()) { reconnectBackoff = 2000; // back to normal once online } else if (millis() - lastReconnectAttempt > reconnectBackoff) { lastReconnectAttempt = millis(); ubidots.reconnect(); ubidots.subscribeLastValue(DEVICE_LABEL, VAR_LED); reconnectBackoff = min(reconnectBackoff * 2, 30000UL); // cap at 30 s }
This is the same non-blocking-timer idea from Project 4, applied to a retry instead of a debounce. - Read the rest of HiveMQ's MQTT Essentials series for topic wildcards, retained messages, and QoS in more depth than fits here.
- Ubidots' own ESP32 DevKitC MQTT guide walks the same wiring and libraries from their side, a useful second reference if a library update ever changes a call in this sketch.
- This is the backbone of your capstone.