The Robot Whisperer: Projects
Four real automations — each with a full wiring diagram, parts list, and firmware — followed by the module that turns them into a business. Everything here assumes you've been through the course up to at least Module 8.
Project P1 — Access control
RFID card door lock
Goal: upgrade the course's smart lock so it opens with a tap of a card — the same experience as a hotel door — with an allow-list you control.
🧒 Picture this
Parts
| Part | Why you need it |
|---|---|
| RC522 reader + cards/fobs | Usually sold as a kit with one card and one keyring fob. 3.3V only! |
| SG90 servo (or solenoid + driver) | The bolt — same one from Module 6. |
| Active buzzer | Feedback: one happy beep = open, three angry beeps = refused. |
| ESP32 + breadboard + wires | Your existing kit. |
Wiring
| ESP32 pin | Connects to | Wire |
|---|---|---|
| 3V3 | RC522 3.3V — NEVER 5V, it kills the reader | red |
| GND | RC522 GND + servo BROWN + buzzer − | black |
| GPIO 5 | RC522 SDA (chip select) | green |
| GPIO 18 | RC522 SCK (clock) | blue |
| GPIO 23 | RC522 MOSI | yellow |
| GPIO 19 | RC522 MISO | purple |
| GPIO 22 | RC522 RST | orange |
| VIN (5V) | servo RED wire | red |
| GPIO 13 | servo ORANGE (signal) | orange |
| GPIO 15 | buzzer + | green |
Seven wires to the reader looks scary, but it's just SPI (one clock, two data lines, one select) plus power. IRQ stays unconnected. The reader is 3.3V-only — wiring its VCC to 5V is the #1 way people kill an RC522.
Step 1 — enrol cards: read their UIDs
Install the MFRC522 library (PlatformIO: miguelbalboa/MFRC522), flash this, and tap every card you want to allow:
#include <SPI.h>
#include <MFRC522.h>
#define SS_PIN 5 // the reader's SDA pin
#define RST_PIN 22
MFRC522 rfid(SS_PIN, RST_PIN);
void setup() {
Serial.begin(115200);
SPI.begin(); // SCK=18, MISO=19, MOSI=23 — ESP32 defaults
rfid.PCD_Init();
Serial.println("Tap a card...");
}
void loop() {
if (!rfid.PICC_IsNewCardPresent() || !rfid.PICC_ReadCardSerial()) return;
Serial.print("Card UID:");
for (byte i = 0; i < rfid.uid.size; i++) {
Serial.printf(" %02X", rfid.uid.uidByte[i]); // hex! (see the Interlude)
}
Serial.println();
rfid.PICC_HaltA(); // stop talking to this card
}Tap a card... Card UID: 9C 7A 12 4B Card UID: 04 D1 33 A2
Step 2 — the lock itself
Copy those UIDs into the allow-list and flash the real firmware:
#include <SPI.h>
#include <MFRC522.h>
#include <ESP32Servo.h>
MFRC522 rfid(5, 22);
Servo bolt;
const int BUZZER = 15;
// The allow-list: UIDs you copied from the enrol sketch.
byte allowed[][4] = {
{0x9C, 0x7A, 0x12, 0x4B}, // JB's card
{0x04, 0xD1, 0x33, 0xA2}, // spare card
};
bool isAllowed(byte* uid, byte size) {
for (auto& card : allowed) {
if (size == 4 && memcmp(uid, card, 4) == 0) return true;
}
return false;
}
void setup() {
Serial.begin(115200);
SPI.begin();
rfid.PCD_Init();
bolt.attach(13);
bolt.write(0); // start LOCKED
pinMode(BUZZER, OUTPUT);
}
void loop() {
if (!rfid.PICC_IsNewCardPresent() || !rfid.PICC_ReadCardSerial()) return;
if (isAllowed(rfid.uid.uidByte, rfid.uid.size)) {
digitalWrite(BUZZER, HIGH); delay(80); digitalWrite(BUZZER, LOW);
bolt.write(90); // open
delay(3000); // (make this non-blocking — Module 7!)
bolt.write(0); // auto-lock
} else {
for (int i = 0; i < 3; i++) { // three angry beeps
digitalWrite(BUZZER, HIGH); delay(60);
digitalWrite(BUZZER, LOW); delay(60);
}
}
rfid.PICC_HaltA();
}Tap an allowed card → happy beep, bolt opens, re-locks after 3s. Tap a stranger's card → three angry beeps, bolt doesn't move.
✅ Project 1 — ship it
Project P2 — Signage
Signpost with lights (WS2812B)
Goal: drive an addressable LED strip — every LED individually controllable — to build shop signs that animate, glow in brand colours, and switch themselves on at dusk.
🧒 Picture this
Parts
| Part | Why you need it |
|---|---|
| WS2812B strip (60 LEDs/m) | Start with 1m. IP65 (rubber-coated) for anything outdoors. |
| 5V power supply, 4A+ | The strip's real food. 60 LEDs at full white ≈ 3.6A — USB cannot do this. |
| 330Ω resistor | In the data line — protects the first LED's input. |
| 1000µF capacitor | Across the strip's power input — absorbs the turn-on surge. |
setBrightness() is also a power-budget tool, not just a looks tool.Wiring
| ESP32 pin | Connects to | Wire |
|---|---|---|
| PSU + | strip 5V pad AND ESP32 VIN | red |
| PSU − | strip GND pad AND ESP32 GND | black |
| GPIO 16 | 330Ω resistor → strip DIN | green |
The strip eats amps, so it drinks straight from the PSU — the ESP32 only supplies the skinny data line. The 330Ω resistor protects the first LED's data pin; the 1000µF capacitor absorbs the power-on surge. Data flows ONE way: connect to the end marked DIN, not DOUT.
First light
Install Adafruit NeoPixel (PlatformIO: adafruit/Adafruit NeoPixel) and run the classic moving-dot test:
#include <Adafruit_NeoPixel.h>
const int PIN = 16;
const int COUNT = 60;
Adafruit_NeoPixel strip(COUNT, PIN, NEO_GRB + NEO_KHZ800);
void setup() {
strip.begin();
strip.setBrightness(80); // 0-255. Start LOW — full white is blinding
strip.show(); // everything off
}
void loop() {
// A moving dot ("chase") in your brand colour.
for (int i = 0; i < COUNT; i++) {
strip.clear();
strip.setPixelColor(i, strip.Color(0x22, 0xD3, 0xEE)); // hex RGB —
strip.show(); // the Interlude pays off
delay(30);
}
}A single cyan dot runs down the strip, over and over. If nothing lights: check DIN vs DOUT (data has a direction!) and that the strip's GND is tied to the ESP32's GND.
Making it a product: the dusk-to-close schedule
A shop sign that someone must remember to switch on is a broken product. The ESP32 has Wi-Fi, so it can simply ask the internet what time it is (a protocol called NTP) and run itself:
#include <WiFi.h>
#include "time.h"
void setup() {
// ...connect Wi-Fi (Module 8)...
// Ask an internet time server what time it is. UTC+3 = Kampala.
configTime(3 * 3600, 0, "pool.ntp.org");
}
bool signShouldBeOn() {
struct tm now;
if (!getLocalTime(&now)) return true; // clock not synced yet? stay on
int minutes = now.tm_hour * 60 + now.tm_min;
return minutes >= 18 * 60 + 30 // on from 18:30...
&& minutes < 23 * 60; // ...off at 23:00
}
void loop() {
if (signShouldBeOn()) {
runAnimation(); // your chase/fade/scroll effect
} else {
strip.clear();
strip.show(); // dark — saves power and bulb life
}
}✅ Project 2 — ship it
/brightness?level=120 endpoint.Project P3 — Switching
Lights from a remote AND an app
Goal: switch a real lamp three ways — an IR remote from the sofa, a phone on the same network, and (via your Module 11 backend) from anywhere on earth.
🧒 Picture this
Parts
| Part | Why you need it |
|---|---|
| 1-channel 5V relay module | The opto-isolated type. Rated 10A/250VAC — plenty for lamps. |
| VS1838B IR receiver | The little black 'eye' that reads remote controls. |
| Any IR remote | A spare TV remote works — we learn whatever codes it sends. |
| 12V bulb + 12V supply | Your SAFE practice lamp before any mains work. |
Wiring
| ESP32 pin | Connects to | Wire |
|---|---|---|
| VIN (5V) | relay VCC | red |
| GND | relay GND + IR eye GND | black |
| GPIO 26 | relay IN (HIGH = click on) | yellow |
| GPIO 35 | IR receiver OUT | purple |
| 3V3 | IR receiver VCC | red |
| — mains — | LIVE → COM, NO → lamp → NEUTRAL | 230V cable |
Everything left of the dashed line is safe 3.3–5V. Everything inside it is 230V mains: the relay's COM/NO contacts sit in the lamp's LIVE wire like a switch. Practice with a 12V lamp first — and have a licensed electrician do the real mains connection.
Step 1 — learn your remote's codes
Install IRremote (PlatformIO: Armin Joachimsmeyer/IRremote). Every button on every remote sends a unique code — sniff them:
#include <IRremote.hpp>
const int IR_PIN = 35;
void setup() {
Serial.begin(115200);
IrReceiver.begin(IR_PIN);
Serial.println("Point any remote at the eye and press buttons...");
}
void loop() {
if (IrReceiver.decode()) {
// Every button sends its own code. Write them down.
Serial.printf("code: 0x%02X\n", IrReceiver.decodedIRData.command);
IrReceiver.resume(); // ready for the next press
}
}Point any remote at the eye and press buttons... code: 0x45 code: 0x46 code: 0x47
Step 2 — remote + app control in one loop
#include <WiFi.h>
#include <WebServer.h>
#include <IRremote.hpp>
const int RELAY = 26;
WebServer server(80);
bool lightOn = false;
void setLight(bool on) {
lightOn = on;
digitalWrite(RELAY, on ? HIGH : LOW); // HIGH = relay clicks closed
}
void setup() {
pinMode(RELAY, OUTPUT);
// ...connect Wi-Fi (Module 8)...
IrReceiver.begin(35);
// Three tiny HTTP endpoints — your phone/app is the remote now.
server.on("/on", [] { setLight(true); server.send(200, "text/plain", "ON"); });
server.on("/off", [] { setLight(false); server.send(200, "text/plain", "OFF"); });
server.on("/toggle", [] {
setLight(!lightOn);
server.send(200, "text/plain", lightOn ? "ON" : "OFF");
});
server.begin();
}
void loop() {
server.handleClient(); // app control
if (IrReceiver.decode()) { // remote control
if (IrReceiver.decodedIRData.command == 0x45) { // your power button
setLight(!lightOn);
}
IrReceiver.resume();
}
}Press the remote's power button → CLICK, lamp toggles. From a phone on the same Wi-Fi: http://192.168.1.57/toggle → lamp toggles, page says ON
✅ Project 3 — ship it
Project P4 — Vision
Camera projects (ESP32-CAM)
Goal: a $7 board that takes photos and streams video — snap whoever presses the doorbell, send it to your backend, and pair it with the smart lock.
🧒 Picture this
Parts
| Part | Why you need it |
|---|---|
| ESP32-CAM (AI-Thinker) + OV2640 | Usually sold together. Get the antenna version for range. |
| FTDI / USB-serial adapter | 3.3V/5V jumper type. This is your programming cable. |
| Solid 5V supply | The camera browns out weak USB ports — a proper 5V/2A source saves hours. |
| microSD card (optional) | For local snapshots without a server. |
The flashing ritual
Connecting...| ESP32 pin | Connects to | Wire |
|---|---|---|
| cam 5V | FTDI 5V (set the FTDI jumper to 5V) | red |
| cam GND | FTDI GND | black |
| cam U0R (receive) | FTDI TX (transmit) — crossed! | green |
| cam U0T (transmit) | FTDI RX (receive) — crossed! | yellow |
| cam IO0 | cam GND — ONLY while flashing | orange jumper |
The ESP32-CAM has no USB port, so an FTDI adapter is its cable to your laptop. TX and RX cross (my transmit is your receive). The IO0→GND jumper puts the chip in flash mode: fit it, press reset, upload — then REMOVE it and press reset again to run.
- Wire it exactly as above — TX/RX crossed, IO0 jumpered to GND.
- Press the board's RST button. It boots into flash mode.
- Upload from PlatformIO (board:
esp32cam). - Remove the IO0 jumper, press RST again — now it runs your code.
First photo
#include "esp_camera.h"
void setup() {
Serial.begin(115200);
camera_config_t cfg = {};
cfg.pixel_format = PIXFORMAT_JPEG;
cfg.frame_size = FRAMESIZE_SVGA; // 800x600 — good speed/size balance
cfg.jpeg_quality = 12; // lower number = better quality
cfg.fb_count = 1;
// ...plus ~16 pin assignments for the AI-Thinker board.
// Copy that block verbatim from the CameraWebServer example —
// every ESP32-CAM project uses the exact same lines.
if (esp_camera_init(&cfg) != ESP_OK) {
Serial.println("Camera init failed");
return;
}
camera_fb_t* fb = esp_camera_fb_get(); // take one photo
Serial.printf("Captured %u bytes of JPEG\n", fb->len);
esp_camera_fb_return(fb); // ALWAYS hand the buffer back
}
void loop() {}Captured 24816 bytes of JPEG
Send it somewhere useful
A photo on a chip helps nobody. POST it to the Go backend from Module 11 — which can store it, timestamp it, and push it to the React dashboard:
#include <HTTPClient.h>
// Send a captured frame to your own backend (the Go server from Module 11).
void sendPhoto(camera_fb_t* fb) {
HTTPClient http;
http.begin("http://your-server.com/api/doorbell");
http.addHeader("Content-Type", "image/jpeg");
int code = http.POST(fb->buf, fb->len); // raw JPEG bytes, no base64
Serial.printf("Upload: HTTP %d\n", code);
http.end();
}Upload: HTTP 200
CameraWebServer example that serves live MJPEG video on the camera's IP — flash it, open the address, and you have a viewfinder for aiming the camera during installs. For the doorbell product: wire the doorbell button to a GPIO, snap on press, upload, and let the backend WhatsApp/notify the owner — "someone is at the gate" with a face attached. Pair it with Project 1 and the dashboard shows who badged in.✅ Project 4 — ship it
Module BIZ — The business
From projects to an automation business
Goal: package the four projects as services, price them sanely, source parts without pain, and build the recurring revenue that makes this a business instead of a hobby.
🧒 Picture this
The services catalogue
Each project you built maps to a service you can quote tomorrow. Clients buy from a menu, not from a parts list:
| Service | What you install | You built it in | Who buys it |
|---|---|---|---|
| Smart access control | Card/fob door locks, entry logs, per-person access, auto-lock | Project 1 + the capstone lock | Offices, apartments & rentals, SACCOs, clinics, schools |
| LED signage & ambience | Animated shop signs, brand-colour lighting, dusk-to-close scheduling | Project 2 | Shops, bars & restaurants, salons, churches, events |
| Remote & app control | Lights, pumps, gates and appliances switched from a remote, phone, or timer | Project 3 | Homes, farms (irrigation, poultry heating), landlords |
| Camera & monitoring | Photo doorbells, gate cameras, 'who badged in' snapshots | Project 4 (+ Project 1) | Gated homes, shops, warehouses |
| Dashboards & integration | One web app showing every device — history, alerts, control | Module 11 (your web skills) | Every client above — this is what makes you look 10× bigger |
Pricing: charge for the outcome, not the parts
The parts for a card-lock install cost roughly $15–25. If you price at parts-plus-a-little, you've invented a badly paid hardware shop. Price the install instead:
- Site survey first, always. A short paid (or credited) visit: measure, photograph, check power and Wi-Fi coverage. It filters non-serious clients and kills nasty surprises.
- Install price ≈ 4–6× parts cost as a starting heuristic — that's parts + your labour + your drive time + a margin for the one component that always arrives dead.
- Quote three tiers. Basic (the thing works), Standard (+ app control), Premium (+ dashboard, history, alerts). Most clients pick the middle one — so design the middle one to be the one you want to sell.
- The dashboard is your differentiator. Any electronics shop can wire a relay. Almost none can hand the client a clean web app with history and alerts. That's your Module 11 superpower — price it like the moat it is.
Recurring revenue: the maintenance contract
One-off installs feed you this month. Contracts feed you every month:
- A monthly fee covers: remote monitoring (is the device online?), signed OTA updates (Module 9), card enrolment/removal, and a same-week fix if something dies.
- Your MQTT backend already knows when a device goes offline — turn that into a WhatsApp alert to yourself and you're running a mini-NOC for a fleet of clients from one laptop.
- Rule of thumb: aim for contract revenue to pass install revenue within a year. Ten clients at a modest monthly fee is real money in Kampala — and it compounds.
Sourcing parts without pain
- Order 5–10× of everything cheap (ESP32s, relays, RC522s, sensors). Shipping dominates cost, boards fail QC, and a client demo you can't do because your only relay died is an expensive lesson.
- AliExpress lead time is 3–6 weeks to East Africa — order for the pipeline, not the current job. Keep a shortlist of local electronics shops for emergencies and pay their premium gladly.
- Test every board the day it arrives (a one-minute blink flash). Discovering a dead board at install time costs you a client; discovering it at your desk costs you nothing.
- Build a spares kit that travels with you: one of everything, plus wire, tape, zip ties, a multimeter, and a powered USB hub.
The demo case: your best salesperson
🧒 Picture this
- Film every install (with permission) — 30-second WhatsApp-ready clips.
- Give your first 2–3 installs at a discount in exchange for being reference sites you can bring prospects to.
- A WhatsApp Business catalogue of your services beats a website for local trade — but the dashboard demo link makes you look enterprise.
The safety & legal line (non-negotiable)
- Hand over a one-page document per install: what was installed, how to use it, the fail-state decision, and what the maintenance contract covers. Module 10's documentation habit, now with an invoice.
- Offer a defined warranty (e.g. 6 months parts + labour). Vague promises create endless free callouts; a written boundary creates contract upsells.
✅ Business milestone — do this before the next install


