PROJECT 13 · CAPSTONE

Capstone: Build Your Own IoT Device

Combine the week into one device that senses, acts, and connects to the cloud, then demo it.

Builds on: everything so far. This is where the twelve separate projects stop being separate.

⏱ ~60 min 📊 Advanced 🔖 Prerequisite: Projects 1-12
⬇ Download the starter sketch The starter already connects to Wi-Fi and Ubidots. Follow the TODO 1/2/3 markers to make it yours.
🎲 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:

  • Combine a sensor, an actuator, and a network service into one device that does something a person would actually want.
  • Scope a build to the time available, which is the skill that decides whether a demo works.
  • Start from a template and extend it, the way most embedded work really begins.
  • Debug a system with several moving parts by testing one at a time.
  • Explain your design to a room: the problem, the solution, and what you would do next.

1The brief

In a team, build a device that does all three:

  1. Senses with at least one sensor (DHT11, PIR, LDR, potentiometer, an obstacle‑avoidance module)
  2. Acts with at least one output (LED, RGB LED, active or passive buzzer, relay, OLED)
  3. Connects to the cloud: a Ubidots dashboard (Project 11) and/or a Telegram bot (Project 12)
Keep the scope small and make it work end to end. A simple thing that fully works beats an ambitious thing that does not.

2Idea menu

IdeaSenseActCloud
Smart room monitorDHT11 + LDROLED readoutUbidots charts
Motion security nodePIRBuzzerTelegram alert
Greenhouse minderDHT11 + LDRLED warningUbidots gauge + event
Cloud appliancebuttonrelayUbidots switch + Telegram
Comfort meterDHT11RGB LED green/yellow/redUbidots dashboard
Obstacle alarmobstacle‑avoidance modulepassive buzzer warning toneTelegram message when something gets close
Light-and-sound alertLDR (darkness trigger)passive buzzer melodyUbidots event when it goes dark

3Getting started

  • Open the starter sketch. It already connects to Wi-Fi and Ubidots, publishes one value, and reacts to one command. Follow the TODO 1/2/3 markers.
  • Steal freely from earlier projects: wiring and code for each part are in the Project Hub and the Components Glossary.
  • Give your device a unique DEVICE_LABEL so your data does not mix with another team's.

4Code Walkthrough: Understanding the Starter Template

The starter is Project 11 with the project-specific parts hollowed out. It compiles and runs as it stands, publishing a constant 0.0, so you can confirm the cloud half works before you touch a sensor. Three TODO markers say where your code goes.

TODO 1: read your sensor

#include "UbidotsEsp32Mqtt.h"
// TODO 1: add your sensor library includes here, e.g.:
// #include <Adafruit_Sensor.h>
// #include <DHT.h>

float readMySensor() {
  // TODO 1: replace this with a real reading from your sensor.
  //   return analogRead(SENSOR_PIN);      // potentiometer / LDR
  //   return dht.readTemperature();       // DHT11
  return 0.0;
}

Everything about your measurement is behind one function. That is deliberate: the rest of the sketch never needs to know whether the number came from an ADC pin, a DHT11, or a calculation across three sensors. Change readMySensor() and the whole device follows.

There are three places to touch for a new sensor, and forgetting the third is the usual bug: the include, the object (for example DHT dht(DHTPIN, DHTTYPE);), and the begin() call in setup().

If your sensor can fail, guard it as Project 9 did: if (isnan(t)) { ... } and skip the publish rather than sending a bad number.

TODO 2: publish what you care about

// TODO 2: add every variable you want to see in the dashboard.
ubidots.add(VAR_SENSOR, value);
ubidots.publish(DEVICE_LABEL);

One add() per variable, then a single publish(). Add a second reading by adding a second const char *VAR_... at the top and a second add() here. Rename VAR_SENSOR to what you are actually measuring (soil, lux, temperature): that name is what appears in Ubidots, so a good one makes the dashboard explain itself.

TODO 3: decide what a command means

void callback(char *topic, byte *payload, unsigned int length) {
  ...
  float v = atof(value);

  // TODO 3: decide what a command means for your project.
  digitalWrite(OUTPUT_PIN, (v >= 0.5) ? HIGH : LOW);
  Serial.printf("[CLOUD] command -> %.1f\n", v);
}

The payload arrives as a number, so a command does not have to be on/off. v could be a brightness for ledcWrite() (Project 3), a target temperature to compare against, or an index selecting which message to show on the OLED (Project 10). Keep the callback short: set a variable or write a pin, and let loop() do anything slow.

What the template already handles for you

CodeWhat it does
connectToWifi() + setup() + reconnect()Gets you onto the network and the broker.
subscribeLastValue(DEVICE_LABEL, VAR_COMMAND)Listens for cloud commands and picks up the current value at boot.
if (!ubidots.connected()) in loop()Reconnects and re-subscribes after a Wi-Fi drop.
millis() - lastPublish > PUBLISH_EVERY_MSPublishes on a timer without blocking.
ubidots.loop()Services MQTT so your callback() actually fires.
Serial.printf("[PUB] ...")Your fastest debugging tool. Keep printing while you build.
Not using Ubidots? If your project is Telegram-based, start from Project 12's sketch instead and apply the same three questions: what do I read, what do I report, what do I do when told.

Pre-demo integration checklist

Work through this once each part works alone, before you trust the whole loop:

  • Sensor prints a sensible value on Serial (not nan, not stuck at one number)
  • Actuator responds to a hardcoded test value before it responds to the sensor
  • Wi‑Fi connects, and reconnects if you toggle the router or walk out of range
  • The cloud side (Ubidots dashboard or Telegram chat) shows the first real reading
  • A cloud command reaches the board and the actuator reacts within a few seconds
  • The whole loop, sense → cloud → actuator, still works after pressing EN/RESET
  • DEVICE_LABEL (or CHAT_ID) is unique to your team, not left at the default
  • Credentials (WIFI_PASS, UBIDOTS_TOKEN, BOT_TOKEN) are filled in, not still placeholders

If a step fails, go back to the single project it came from (Project 9 for a sensor, Project 11 or 12 for the cloud link) and confirm it still works in isolation before debugging the combination.

Predict, then check. Before you start building: which of the three links (sense, act, cloud) do you predict will be the one that breaks first? Build that one first, and see if you were right.

5What to show at the demo (about 3 minutes)

  1. What problem does your device solve, and for whom?
  2. Show it working: trigger the sensor, watch the actuator and the dashboard or chat respond.
  3. One thing that was harder than expected, and how you handled it.
  4. One thing you would add with another day.

6Grading (10 points)

CriteriaPoints
Sensor reads real, sensible values2
Actuator responds correctly2
Data reaches the cloud3
Full loop works live at the demo2
Clear explanation of the idea1

Bonus (up to 2): a cloud alert/event, a clean OLED display, or a genuinely creative idea.

7Finish on time

  • Wire and test one part at a time, confirm good Serial values before adding the cloud.
  • Use millis(), not delay(), so Wi-Fi and MQTT stay responsive.
  • Keep a known-working version before adding the next feature.

🎯 Knowledge Check

1. What is the main purpose of the starter template?

2. What should you do first when combining sensor + actuator + cloud?

3. What does the pre-demo integration checklist help you verify?

4. What is the recommended demo structure?

9Wrapping Up: What You've Learned

Look back at the week. Every project was one idea, and the capstone is where they stop being separate:

  • Read the world, decide, act on the world (Project 1). Everything since has been a richer version of that loop.
  • Digital or analog, raw counts or physical units (Projects 2, 3, 9). Knowing which kind of sensor you have tells you how much interpreting is your job.
  • millis(), never delay() (Project 4). Not a style rule: it is what lets a device do two things at once, and every networked project since has depended on it.
  • The board can serve a page (Projects 5 to 9): great on the local network, invisible from outside it.
  • State has to be kept honest (Project 8). When two interfaces control one output, they both have to be told the truth.
  • Local output has its place (Project 10). Not everything needs a phone.
  • Publish/subscribe reaches anywhere (Project 11), and an existing app can be your UI (Project 12).

Now try

  • OTA updates: Add over-the-air update capability so you can upload new sketches without connecting the USB cable.
  • Web dashboard: Add a simple web page (like Project 9) that shows the same sensor data as the Telegram bot, giving you two ways to view the data.

The engineering habits matter as much as the APIs: check that hardware started, guard against bad readings, keep credentials out of your code's public life, print enough on Serial to see what is happening, and keep a version that works before adding the next thing.

That is a real IoT skill set. The next device you build is the same shape, with different parts.