Learning Goals 5 min
Cluster G's capstone is the project that always sells the IoT idea: press a button, anywhere in the world someone's phone buzzes. We've had all the pieces — WiFi, NTP, webhooks, IFTTT — and today we mount them into a real doorbell-shaped device. By the end of this lesson you will:
- Build a finished "DM doorbell": button + ESP + buzzer + LED, mounted in a small enclosure, powered by USB or battery.
- Add three layers of feedback: visible LED "sending" flash, audible buzzer chime, and remote phone notification — so both the visitor and the home owner know the doorbell worked.
- Add resilience: WiFi reconnection, NTP re-sync, webhook retry on failure, queued events that survive a brief outage.
Warm-Up 10 min
Pull together the kit for today:
- ESP (NodeMCU or ESP32).
- Push button — any tactile button is fine; a real doorbell-button is bonus.
- Piezo buzzer for the audible chime.
- LED + 220 Ω resistor for the "sending" indicator.
- USB cable or a 5 V USB power bank for untethered power.
- Cardboard / plastic box for the enclosure.
- IFTTT applet from yesterday + a Discord webhook URL.
Product specification first
Before coding, write the spec in your notebook:
- Visitor experience: presses button → hears chime within 100 ms → sees LED flash → walks away.
- Owner experience: within 10 s, phone vibrates with "Doorbell at 19:30" notification. Also a Discord log entry.
- Outage behaviour: WiFi drops? Phone still gets the notification within 30 s once the connection comes back. Buzzer + LED never affected.
This spec drives the architecture in §3.
New Concept · Local-first, network-second 20 min
Why local feedback first
The buzzer + LED must fire immediately on button press — synchronous, in the interrupt or main loop. The webhook then happens after local feedback completes. If the network is slow, the visitor still gets a real chime.
Bad order (don't): button → webhook (takes ~3 s) → buzzer. The visitor stands awkwardly waiting, presses again, you get spam alerts.
Good order: button → buzzer + LED → webhook in background. We'll keep the webhook synchronous in the loop for simplicity, but immediately after the local feedback so the perceived delay is zero.
Retry queue for offline events
If WiFi is down when the bell rings, we don't want to lose the event. Store the timestamp in a queue; flush when the connection comes back.
#define QUEUE_SIZE 5
time_t queue[QUEUE_SIZE];
int qHead = 0, qTail = 0, qCount = 0;
bool enqueue(time_t t) {
if (qCount == QUEUE_SIZE) return false; // queue full, drop event
queue[qTail] = t;
qTail = (qTail + 1) % QUEUE_SIZE;
qCount++;
return true;
}
bool peek(time_t& t) {
if (qCount == 0) return false;
t = queue[qHead];
return true;
}
void dropHead() {
qHead = (qHead + 1) % QUEUE_SIZE;
qCount--;
}A small ring buffer. Holds up to 5 unsent doorbell events. On every loop iteration, if WiFi is connected, try to send the head of the queue; on success, drop the head.
The three-layer feedback timing
| Layer | Trigger | Latency target |
|---|---|---|
| 1. Buzzer + LED | Button press | < 50 ms |
| 2. Discord embed | After local feedback | ~200–500 ms |
| 3. Phone push notification (IFTTT) | After Discord | ~5–15 s |
The visitor only sees layer 1. The owner sees layer 3. Layer 2 is the developer's "always-on log" — handy for debugging weeks later when someone says "the doorbell never worked yesterday" and you can check the Discord channel.
Worked Example · The finished DM doorbell 25 min
Step 1 — wire it
| Component | ESP pin |
|---|---|
| Doorbell button | D7 (GPIO13), INPUT_PULLUP |
| Buzzer + | D6 (GPIO12) |
| Buzzer − | GND |
| LED + 220 Ω | D5 (GPIO14) |
Step 2 — the sketch
// L03-38 · Doorbell That DMs You
// Button -> buzzer + LED -> Discord embed -> IFTTT phone notification
// With offline event queue: events captured during WiFi outage are flushed on reconnect.
#if defined(ESP8266)
#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <WiFiClientSecureBearSSL.h>
using SecureClient = BearSSL::WiFiClientSecure;
#elif defined(ESP32)
#include <WiFi.h>
#include <HTTPClient.h>
#include <WiFiClientSecure.h>
using SecureClient = WiFiClientSecure;
#endif
#include <ArduinoJson.h>
#include <time.h>
const char* SSID = "YourNetwork";
const char* PASSWORD = "YourPassword";
const char* DISCORD_URL = "https://discord.com/api/webhooks/REPLACE_ME";
const char* IFTTT_URL = "https://maker.ifttt.com/trigger/doorbell/with/key/REPLACE_ME";
const int BTN_PIN = 13;
const int BUZZER = 12;
const int LED = 14;
const unsigned long DEBOUNCE_MS = 50;
const unsigned long COOLDOWN_MS = 5000;
bool lastBtn = HIGH;
unsigned long lastEdge = 0;
unsigned long lastSentAt = 0;
#define QUEUE_SIZE 5
time_t qBuf[QUEUE_SIZE];
int qHead = 0, qTail = 0, qCount = 0;
bool enqueue(time_t t) {
if (qCount == QUEUE_SIZE) { Serial.println("# queue full, dropping");
return false;
}
qBuf[qTail] = t;
qTail = (qTail + 1) % QUEUE_SIZE;
qCount++;
return true;
}
void playChime() {
// 2-note chime, ~150 ms total
tone(BUZZER, 880, 70);
delay(80);
tone(BUZZER, 1320, 70);
}
void flashLED() {
digitalWrite(LED, HIGH);
// We let loop turn it off after the webhook
}
bool postJSON(const char* url, const String& body) {
if (WiFi.status() != WL_CONNECTED) return false;
SecureClient client;
client.setInsecure();
HTTPClient http;
http.begin(client, url);
http.addHeader("Content-Type", "application/json");
int status = http.POST(body);
Serial.print("# POST -> ");
Serial.println(status);
http.end();
return status >= 200 && status < 300;
}
bool sendEvent(time_t t) {
struct tm tmEv;
localtime_r(&t, &tmEv);
char timeBuf[20];
strftime(timeBuf, sizeof(timeBuf), "%H:%M:%S", &tmEv);
// Discord embed
StaticJsonDocument<384> doc;
JsonObject embed = doc["embeds"].createNestedObject();
embed["title"] = "Doorbell pressed";
embed["description"] = "Someone's at the door.";
embed["color"] = 0x58A4FF;
JsonArray fields = embed.createNestedArray("fields");
JsonObject f1 = fields.createNestedObject();
f1["name"] = "Time";
f1["value"] = timeBuf;
f1["inline"] = true;
String dBody;
serializeJson(doc, dBody);
if (!postJSON(DISCORD_URL, dBody)) return false;
// IFTTT phone notification
String iBody = "{\"value1\":\"";
iBody += timeBuf;
iBody += "\"}";
postJSON(IFTTT_URL, iBody); // failure here doesn't block the queue drop
return true;
}
void flushQueue() {
if (qCount == 0 || WiFi.status() != WL_CONNECTED) return;
if (millis() - lastSentAt < 1000) return; // rate limit ourselves a little
time_t t = qBuf[qHead];
if (sendEvent(t)) {
qHead = (qHead + 1) % QUEUE_SIZE;
qCount--;
lastSentAt = millis();
Serial.print("# Sent. Queue depth = ");
Serial.println(qCount);
digitalWrite(LED, LOW); // clear "pending" LED when queue empties
if (qCount == 0) digitalWrite(LED, LOW);
}
}
void onPress() {
if (millis() - lastSentAt < COOLDOWN_MS) return;
Serial.println("# Bell pressed");
flashLED(); // local "received" indicator
playChime(); // local audible chime
time_t now = time(nullptr);
if (now < 24 * 3600) now = millis() / 1000; // NTP not ready -> use uptime
enqueue(now);
}
void checkButton() {
bool b = digitalRead(BTN_PIN);
if (b == lastBtn) return;
if (millis() - lastEdge < DEBOUNCE_MS) return;
lastEdge = millis();
lastBtn = b;
if (b == LOW) onPress();
}
void setup() {
Serial.begin(115200);
pinMode(BTN_PIN, INPUT_PULLUP);
pinMode(BUZZER, OUTPUT);
pinMode(LED, OUTPUT);
digitalWrite(LED, LOW);
WiFi.mode(WIFI_STA);
WiFi.begin(SSID, PASSWORD);
unsigned long t0 = millis();
while (WiFi.status() != WL_CONNECTED && millis() - t0 < 20000) {
delay(500);
Serial.print(".");
}
Serial.println();
configTime(0, 0, "pool.ntp.org");
setenv("TZ", "UTC0", 1); // your zone (L03-35)
tzset();
Serial.println("# Doorbell armed.");
}
void loop() {
// Maintain WiFi
if (WiFi.status() != WL_CONNECTED) {
static unsigned long lastTry = 0;
if (millis() - lastTry > 5000) {
lastTry = millis();
WiFi.disconnect();
WiFi.begin(SSID, PASSWORD);
}
}
checkButton();
flushQueue();
}Step 3 — bench test
- Press the button. You hear "ding-ding" immediately. LED lights up. Within ~2 s, Discord shows the embed; within ~10 s, the phone notification.
- Disconnect your WiFi (turn off the hotspot). Press the button. Local chime + LED fire as before; LED stays on (queue has one pending). The serial log shows queued status.
- Re-enable WiFi. Within ~5 s, the queued event flushes; LED goes off; phone gets the notification with the correct earlier timestamp.
Step 4 — build the enclosure
Cardboard box, button on top, ESP + buzzer + LED inside. Wires tidy. USB cable out the back. Label "Press for delivery" or whatever fits your context. Stick it on the front door / fridge / desk.
Step 5 — leave it running for 24 hours
Put it somewhere. Have family / housemates press it occasionally. Check the Discord channel — you have a free, durable log of all activity. Check IFTTT — phone notifications fire on schedule.
Step 6 — measure end-to-end latency in practice
Press the button. Watch the phone (have a stopwatch). Average over 5 trials. Typical: chime 0 ms, Discord embed 200 ms, IFTTT phone notification 6–12 s. Tell visitors "wait 5 seconds".
Basic 5 min
Goal: Change the chime to a melody — three notes of any tune (e.g. NBC's G-E-C chime).
Challenge 1 5 min
Goal: Add a PIR motion sensor on D8. Fire the webhook (without chime) if someone approaches but doesn't press. Different IFTTT event (doorbell_motion) → different phone notification ("Someone's at the door, no press").
Challenge 2 5 min
Goal: Add a photo URL to the Discord embed. (You don't have a camera, but you can pretend.) Discord embeds support an image field — if you have a static placeholder image online, include it.
Challenge 3 · Test the offline queue 10 min
Read the Worked Example sketch closely, then predict before you test.
- WiFi is off. Someone presses the bell 8 times, 10 seconds apart. How many events go into the queue? How many are dropped? (Check what
lastSentAtdoes while offline.) - WiFi comes back.
flushQueue()sends at most one event per second. Roughly how long until the queue is empty? - The LED is meant to stay on while any event is waiting. Find the line in
flushQueue()that breaks this. Fix it. - Run the test on the bench and compare it with your predictions.
It works if your bench test matches your predictions, and the LED stays on until the very last queued event is sent.
Recap 5 min
The DM doorbell glues everything from Cluster F + G: WiFi, NTP, dual webhooks. The new ideas were local-first feedback (chime / LED before webhook) and a small retry queue for offline resilience. The architecture is exactly how real connected doorbells (Ring, Nest) work, minus the camera and the cloud — and the cloud is just a server instead of Discord. With a webhook URL and 100 lines of code you can replicate an expensive commercial product's core function. Cluster H next: making your code reusable so the next project doesn't need this much re-typing.
- Local-first feedback
- Trigger local outputs (sound, LED) before any network call so the user's perceived latency is zero.
- Offline queue
- A small in-memory buffer of unsent events. On network restore, flush in order. Stops transient outages from losing events.
- Ring buffer
- A fixed-size circular queue. Head and tail pointers wrap around the array. Constant-time enqueue / dequeue; no dynamic allocation.
- Cooldown
- Minimum time between accepted events to prevent button-mashing spam.
- Three-layer feedback
- Three signals at different latency targets: instant (local hardware), fast (Discord log), eventual (phone push). Each audience sees the appropriate one.
- WiFi reconnection logic
- Code that detects WiFi drops and re-issues
WiFi.begin()at intervals. Required for any unattended device. - Resilience
- The property of continuing to work through partial failures. For us: button still chimes locally even when WiFi is down; events still arrive in order once it recovers.
Extra Mission 5 min
Part 1 — Design a finished product
Your doorbell works on a breadboard. Now design it as a product someone else could use without you there. Pick where it goes: a front door, a bedroom door, a shop counter, a classroom.
Your design must include:
- A drawing of the enclosure, with the button, LED and buzzer positions marked.
- The label on the button, and what a visitor sees and hears after one press.
- What happens if they press it 10 times in a row, and why you chose that.
- What happens when WiFi drops, and how the owner can tell.
- How it gets power, and where the cable goes.
Part 2 — Make it
Build your design: the fixed doorbell sketch inside a box (cardboard or a plastic tub), with tidy wires and a clear label. Then hand it to someone at home and watch them use it without help.
Bring back next class: a photo of the finished device, your uploaded sketch, and a 30-second video: press, chime, Discord embed, phone notification. Include one press with WiFi off.