PROJECT 11 · CLOUD / MQTT

MQTT + Ubidots: Cloud Dashboard and Control

Send DHT11 readings to a live cloud dashboard, and switch an LED or relay back from anywhere.

Builds on: Project 9's DHT11 reading and Project 7's actuator, now published to and controlled from the cloud instead of a local page.

⏱ ~35 min 📊 Intermediate 🔖 Prerequisite: Project 9
⬇ Download the sketch Save 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.
🎲 Try it without hardware 📄 diagram.json Open the Wokwi simulator, then open or download 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.

A phone publishes 'open door' to an MQTT broker, which forwards it to a smart home subscribed to the same topic
Source: HiveMQ, "MQTT Essentials Part 1: Introducing MQTT"
TermMeaningHere
BrokerServer that routes all messagesHosted by Ubidots
PublishSend a value to a topicESP32 sends temperature, humidity
SubscribeAsk to receive a topicESP32 listens for led
QoSHow 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.

DHT11 --> ESP32 --publish--> [ Ubidots broker ] --> Dashboard (you watch) LED <-- ESP32 <--subscribe-- [ Ubidots broker ] <-- Switch widget (you control)

2Get your Ubidots token

  1. Sign up for a free Ubidots STEM account at stem.ubidots.com.
  2. Open your profile, then API Credentials, and copy the Default token (looks like BBUS-xxxx).
  3. 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

QtyPartNotes
1ESP32 · breadboard · jumper wiresas always
1DHT11 moduleS to GPIO 4, + to 3.3 V, - to GND (same as Project 9)
1LED + 220 Ohm, or the relay from Project 7to GPIO 26 (the thing the cloud switches)
This is Project 9's sensor plus Project 7's actuator on one breadboard. Nothing new to wire, only new code.

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.

The token is a password. Anyone holding it can write data as your device, so keep it out of screenshots and out of version control. 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:

  1. 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.
  2. Publish on a schedule, using Project 4's millis() timer. Two add() calls then one publish() sends both readings in a single message.
  3. Give the library time to work. 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.

This is why 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

CodeWhat 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_MSProject 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 bufferTurns a raw MQTT payload into a proper C string before atof().
Predict, then check. Predict: if you turn off your phone's Wi‑Fi and flip the dashboard switch using cellular data instead, does the board still respond? Try it, then think about why publish/subscribe makes that possible.

7Build the dashboard

  1. In Devices, confirm your device appeared with temperature and humidity.
  2. Add a control variable: open the device, Add Variable, type Raw, name it exactly led.
  3. In Data, Dashboards, add a Gauge on temperature, a Line chart on temperature and humidity, and a Switch on led.
  4. 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 led command). 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

SymptomLikely causeFix
Serial stops after Wi-FiWrong tokenRe-copy the Default token
No device in UbidotsNever publishedCheck DHT reads numbers, not --
Switch does nothingVariable not named ledName it exactly led
Reconnect loop5 GHz Wi-FiESP32 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 the led command (Ubidots remembers the last value and hands it over on subscribe); a raw MQTT publish(topic, payload, true) (the trailing true is 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 raw PubSubClient (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 retries ubidots.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.