I Built a Smart LED Toy for My 3-Year-Old Son Using a $4 Chip — Here’s What Almost Broke Me
The Night I Almost Gave Up
It was 11:30 PM. My son Debansh was asleep. I was sitting on the floor with three ESP32 boards, a half-soldered LED ring, and a Serial Monitor screaming WiFi FAILED. Status code: 6 at me — again.
I had spent four hours trying to connect one tiny microcontroller to my home WiFi — something that sounds simple, but is actually a core part of what IoT means in real-world projects.. My router was fine. My password was correct. The board kept disconnecting like it had a personal grudge against me.
Turned out? My router was running 5GHz only. And ESP32 cannot see 5GHz. At all. It literally does not exist for this chip.
That one realization — which took me four hours to find — is the reason I’m writing this blog. So you don’t lose your Saturday night to the same wall.
| This is not a ‘buy this, connect that, it works’ tutorial. This is the real story — with real errors, real debug steps, and the exact code that runs on my living room shelf right now — blinking happily for Debansh every single evening. |
What We’re Actually Building
Before we get into the weeds, here’s the full picture of what this project does:
- 20 LED light effects — Rainbow, Fire, Meteor Rain, Police Lights, Candy (kid-favourite), and 15 more
- Mobile web dashboard — No app install. Just open a browser on your phone
- On/Off power button — One tap to kill all LEDs without unplugging anything
- Learn & Play game — Shows random A–Z letters and 0–9 numbers, Debansh taps the right answer
- Sparkle celebration — When he gets it right, the whole ring explodes in rainbow sparkles
- Parent controls — Choose which letters and numbers appear before starting the game
Total hardware cost: under ₹500 / $6 USD. Total hours to build if you follow this guide: under 2 hours.
The Shopping List (And Why Each Part Was Chosen)
I tried two other LED types before landing on WS2812. Here’s what the table actually looks like based on what I observed, not just what the datasheets say:
| Component | What I Paid (India) | Can You Skip It? |
| ESP32 Dev Board (Choose best boards for IoT projects) | ₹320 (~$4) | No — the brain |
| WS2812 16-LED Ring | ₹150 (~$2) | No — the show |
| USB Micro Cable | ₹40 | No — for power + upload |
| 300Ω Resistor (optional) | ₹2 | Recommended |
| Jumper wires (3 pcs) | ₹20 | No |
Why This Matters: The 300Ω resistor between ESP32’s D2 pin and the LED ring’s DI pin isn’t in most tutorials. But without it, long wires pick up noise and your LEDs flicker randomly. I learned this the hard way after 20 minutes of debugging what I thought was a code bug.
Wiring: Three Wires. That’s Literally It.
Think of the LED ring like a garden sprinkler. It needs:
- Water pressure → That’s your 5V power (VIN pin)
- A drain → That’s GND
- A control signal → That’s the data wire (D2 pin)
| WS2812 Ring Pin | ESP32 Pin | Wire Colour (suggested) |
| 5V | VIN | Red |
| GND | GND | Black |
| DI (Data In) | D2 | Yellow or Blue |
| DO (Data Out) | Not connected | — |
| ⚠️ CRITICAL: Use VIN, not 3.3V. The LED ring needs 5 volts to display correct colours. On 3.3V the ring appears dim, colours look wrong, and the first LED often shows a different hue than the rest. I wasted an hour before I swapped the wire. |
Why This Matters: The DO pin (Data Out) is only needed if you chain two rings together. One ring = DO is left floating, completely unconnected. Plugging it in randomly to some GPIO pin caused my first board to bootloop until I removed it.

Tested & Working Code
// ============================================================
// 🌟 Dedicated to Debansh — My Son 🌟
// WS2812 16-LED Ring Controller + Learn & Play Game
// Board : ESP32 (any variant)
// Library: FastLED (install from Arduino Library Manager)
// ============================================================
//
// WIRING:
// ┌──────────────────┬──────────────────┐
// │ WS2812 Ring Pin │ ESP32 Pin │
// ├──────────────────┼──────────────────┤
// │ 5V │ VIN (5 V) │
// │ GND │ GND │
// │ DI (Data In) │ D2 (GPIO 2) │
// │ DO (Data Out) │ Not connected │
// └──────────────────┴──────────────────┘
// ============================================================
#include <FastLED.h>
#include <WiFi.h>
#include <WebServer.h>
#include <esp_wifi.h> // needed for esp_wifi_set_ps()
// ──────────────────────────────────────────────
// ★ CHANGE THESE TO YOUR WIFI DETAILS ★
// ──────────────────────────────────────────────
const char* WIFI_SSID = "YourWiFiName";
const char* WIFI_PASSWORD = "YourWiFiPassword";
// ──────────────────────────────────────────────
#define LED_PIN 2
#define NUM_LEDS 16
#define LED_TYPE WS2812B
#define COLOR_ORDER GRB
CRGB leds[NUM_LEDS];
WebServer server(80);
// ── Global state ──────────────────────────────
int currentEffect = 0;
int effectSpeed = 50;
int brightness = 180;
CRGB solidColor = CRGB::Blue;
bool ledsOn = true;
unsigned long lastUpdate = 0;
int effectStep = 0;
// ── Game: celebrate sparkle timer ─────────────
bool celebrateActive = false;
unsigned long celebrateStart = 0;
#define CELEBRATE_DURATION 3000 // ms
// ── Effect names ──────────────────────────────
const char* effectNames[] = {
"Rainbow",
"Rainbow Cycle",
"Color Wipe",
"Theater Chase",
"Comet",
"Meteor Rain",
"Larson Scanner",
"Fire Effect",
"Twinkle",
"VU Meter",
"Solid Color",
"Breathing",
"Sparkle",
"Color Bounce",
"Police Lights",
"Confetti",
"Color Pulse",
"Starburst",
"Waterfall",
"Candy"
};
const int NUM_EFFECTS = 20;
// ============================================================
// SETUP
// ============================================================
void setup() {
Serial.begin(115200);
delay(400);
Serial.println("\n\n=== Debansh LED Ring Controller ===");
FastLED.addLeds<LED_TYPE, LED_PIN, COLOR_ORDER>(leds, NUM_LEDS);
FastLED.setBrightness(brightness);
fill_solid(leds, NUM_LEDS, CRGB::Black);
FastLED.show();
Serial.print("Connecting to WiFi: ");
Serial.println(WIFI_SSID);
// ── Deep reset + force 2.4GHz STA mode ──────────────
WiFi.persistent(false); // do not save credentials to flash
WiFi.disconnect(true); // disconnect + clear old config
WiFi.mode(WIFI_OFF); // full radio off
delay(1500);
WiFi.mode(WIFI_STA);
esp_wifi_set_ps(WIFI_PS_NONE); // disable power-save (fixes status=6)
delay(300);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
Serial.print("Trying");
int tries = 0;
while (WiFi.status() != WL_CONNECTED && tries < 40) {
delay(500);
Serial.print(".");
if (tries % 10 == 9) {
Serial.print(" ["); Serial.print(WiFi.status()); Serial.print("] ");
}
tries++;
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println("\n\nWiFi connected!");
Serial.print("IP Address: http://");
Serial.println(WiFi.localIP());
Serial.print("Signal strength (RSSI): ");
Serial.print(WiFi.RSSI());
Serial.println(" dBm");
} else {
Serial.println("\n\nWiFi FAILED (status=6 = kept disconnecting).");
Serial.println("Almost certainly a 5GHz problem. Try these:");
Serial.println(" 1. Turn on your PHONE HOTSPOT (2.4GHz by default)");
Serial.println(" and change WIFI_SSID/WIFI_PASSWORD to your hotspot.");
Serial.println(" 2. OR log into your router and enable 2.4GHz band.");
Serial.println(" 3. OR move ESP32 closer to the router.");
Serial.println("Web server running but unreachable until WiFi works.");
}
server.on("/", handleRoot);
server.on("/set", handleSet);
server.on("/status", handleStatus);
server.on("/celebrate", handleCelebrate);
server.on("/power", handlePower);
server.begin();
Serial.println("Web server started.");
}
// ============================================================
// MAIN LOOP
// ============================================================
void loop() {
server.handleClient();
unsigned long now = millis();
if (now - lastUpdate >= (unsigned long)effectSpeed) {
lastUpdate = now;
if (!ledsOn) {
fill_solid(leds, NUM_LEDS, CRGB::Black);
FastLED.show();
return;
}
// Celebrate (correct answer sparkle) overrides effects
if (celebrateActive) {
if (now - celebrateStart < CELEBRATE_DURATION) {
effectCelebrate();
} else {
celebrateActive = false;
}
} else {
runCurrentEffect();
}
FastLED.setBrightness(brightness);
FastLED.show();
effectStep++;
}
}
// ============================================================
// EFFECTS DISPATCHER
// ============================================================
void runCurrentEffect() {
switch (currentEffect) {
case 0: effectRainbow(); break;
case 1: effectRainbowCycle(); break;
case 2: effectColorWipe(); break;
case 3: effectTheaterChase(); break;
case 4: effectComet(); break;
case 5: effectMeteorRain(); break;
case 6: effectLarsonScanner(); break;
case 7: effectFire(); break;
case 8: effectTwinkle(); break;
case 9: effectVUMeter(); break;
case 10: effectSolidColor(); break;
case 11: effectBreathing(); break;
case 12: effectSparkle(); break;
case 13: effectColorBounce(); break;
case 14: effectPoliceLights(); break;
case 15: effectConfetti(); break;
case 16: effectColorPulse(); break;
case 17: effectStarburst(); break;
case 18: effectWaterfall(); break;
case 19: effectCandy(); break;
}
}
// ============================================================
// ORIGINAL EFFECTS (0–12)
// ============================================================
void effectRainbow() {
uint8_t hue = effectStep * 3;
for (int i = 0; i < NUM_LEDS; i++)
leds[i] = CHSV(hue + (i * 256 / NUM_LEDS), 255, 255);
}
void effectRainbowCycle() {
uint8_t hue = effectStep * 5;
for (int i = 0; i < NUM_LEDS; i++)
leds[i] = CHSV(hue + (i * 256 / NUM_LEDS), 240, 255);
}
void effectColorWipe() {
int pos = effectStep % (NUM_LEDS * 2);
if (pos == 0) fill_solid(leds, NUM_LEDS, CRGB::Black);
if (pos < NUM_LEDS) leds[pos] = solidColor;
else leds[pos - NUM_LEDS] = CRGB::Black;
}
void effectTheaterChase() {
fill_solid(leds, NUM_LEDS, CRGB::Black);
for (int i = 0; i < NUM_LEDS; i++)
if ((i + effectStep) % 3 == 0) leds[i] = solidColor;
}
void effectComet() {
for (int i = 0; i < NUM_LEDS; i++) leds[i].fadeToBlackBy(80);
int pos = effectStep % NUM_LEDS;
leds[pos] = solidColor;
if (pos > 0) leds[pos-1] = solidColor.scale8(180);
if (pos > 1) leds[pos-2] = solidColor.scale8(80);
}
void effectMeteorRain() {
for (int i = 0; i < NUM_LEDS; i++)
if (random8(3) != 0) leds[i].fadeToBlackBy(64);
int pos = effectStep % NUM_LEDS;
leds[pos] = CRGB::White;
if (pos > 0) leds[pos-1] = CRGB(100,100,255);
if (pos > 2) leds[pos-2] = CRGB(30,30,80);
}
void effectLarsonScanner() {
fadeToBlackBy(leds, NUM_LEDS, 40);
int period = NUM_LEDS * 2 - 2;
int pos = effectStep % period;
if (pos >= NUM_LEDS) pos = period - pos;
leds[pos] = CRGB::Red;
if (pos > 0) leds[pos-1] = CRGB(120,0,0);
if (pos < NUM_LEDS-1) leds[pos+1] = CRGB(120,0,0);
}
static byte heat[NUM_LEDS];
void effectFire() {
for (int i = 0; i < NUM_LEDS; i++)
heat[i] = qsub8(heat[i], random8(0,40));
for (int i = NUM_LEDS-1; i >= 2; i--)
heat[i] = (heat[i-1] + heat[i-2] + heat[i-2]) / 3;
if (random8(255) < 100) {
int y = random8(4);
heat[y] = qadd8(heat[y], random8(160,255));
}
for (int i = 0; i < NUM_LEDS; i++)
leds[i] = HeatColor(heat[i]);
}
void effectTwinkle() {
for (int i = 0; i < NUM_LEDS; i++) leds[i].fadeToBlackBy(20);
if (random8(3) == 0) leds[random8(NUM_LEDS)] = CHSV(random8(), 200, 255);
}
void effectVUMeter() {
int level = (int)(8.0 + 7.5 * sin(effectStep * 0.3));
fill_solid(leds, NUM_LEDS, CRGB::Black);
for (int i = 0; i < level && i < NUM_LEDS; i++) {
if (i < 10) leds[i] = CRGB::Green;
else if (i < 14) leds[i] = CRGB::Yellow;
else leds[i] = CRGB::Red;
}
}
void effectSolidColor() {
fill_solid(leds, NUM_LEDS, solidColor);
}
void effectBreathing() {
float val = (exp(sin(effectStep * 0.07)) - 0.36787944) * 108.0;
uint8_t bright = (uint8_t)constrain(val, 0, 255);
fill_solid(leds, NUM_LEDS, solidColor);
FastLED.setBrightness(bright);
}
void effectSparkle() {
fill_solid(leds, NUM_LEDS, CRGB::Black);
for (int i = 0; i < 3; i++) leds[random8(NUM_LEDS)] = CRGB::White;
}
// ============================================================
// NEW EFFECTS (13–19)
// ============================================================
// 13: Color Bounce — two coloured dots chase each other
void effectColorBounce() {
fill_solid(leds, NUM_LEDS, CRGB::Black);
int period = NUM_LEDS * 2 - 2;
int p1 = effectStep % period;
int p2 = (effectStep + NUM_LEDS/2) % period;
if (p1 >= NUM_LEDS) p1 = period - p1;
if (p2 >= NUM_LEDS) p2 = period - p2;
leds[p1] = CRGB::Cyan;
leds[p2] = CRGB::Magenta;
if (p1 > 0) leds[p1-1] = CRGB(0,60,60);
if (p2 > 0) leds[p2-1] = CRGB(60,0,60);
}
// 14: Police Lights — alternating red/blue flash
void effectPoliceLights() {
fill_solid(leds, NUM_LEDS, CRGB::Black);
bool redPhase = (effectStep / 4) % 2 == 0;
for (int i = 0; i < NUM_LEDS/2; i++) leds[i] = redPhase ? CRGB::Red : CRGB::Black;
for (int i = NUM_LEDS/2; i < NUM_LEDS; i++) leds[i] = !redPhase ? CRGB::Blue : CRGB::Black;
}
// 15: Confetti — random coloured flashes on dim background
void effectConfetti() {
fadeToBlackBy(leds, NUM_LEDS, 10);
int pos = random8(NUM_LEDS);
leds[pos] += CHSV(effectStep * 3 + random8(64), 200, 255);
}
// 16: Color Pulse — whole ring pulses through hues
void effectColorPulse() {
uint8_t hue = (effectStep * 2) % 256;
uint8_t wave = beatsin8(20, 80, 255);
fill_solid(leds, NUM_LEDS, CHSV(hue, 255, wave));
}
// 17: Starburst — two dots shoot from center outward
void effectStarburst() {
fadeToBlackBy(leds, NUM_LEDS, 60);
int pos = effectStep % (NUM_LEDS / 2);
int mid = NUM_LEDS / 2;
leds[(mid + pos) % NUM_LEDS] = CHSV(effectStep * 4, 255, 255);
leds[(mid - pos + NUM_LEDS) % NUM_LEDS] = CHSV(effectStep * 4 + 128, 255, 255);
}
// 18: Waterfall — cascading blue/cyan drips
void effectWaterfall() {
for (int i = NUM_LEDS-1; i > 0; i--) leds[i] = leds[i-1];
uint8_t v = random8(2) == 0 ? random8(150, 255) : 0;
leds[0] = CHSV(140 + random8(30), 220, v);
}
// 19: Candy — slow pastel rotation, great for kids!
void effectCandy() {
CRGB colors[4] = { CRGB(255,100,150), CRGB(100,255,150), CRGB(100,150,255), CRGB(255,220,80) };
for (int i = 0; i < NUM_LEDS; i++) {
int idx = ((i + effectStep / 3) % 4);
leds[i] = colors[idx];
}
}
// ============================================================
// CELEBRATE EFFECT — triggered by correct game answer
// ============================================================
void effectCelebrate() {
for (int i = 0; i < NUM_LEDS; i++) {
leds[i] = CHSV(random8(), 255, random8(200, 255));
}
}
// ============================================================
// WEB SERVER HANDLERS
// ============================================================
void handlePower() {
if (server.hasArg("on")) {
ledsOn = server.arg("on") == "1";
}
server.send(200, "text/plain", ledsOn ? "1" : "0");
}
void handleCelebrate() {
celebrateActive = true;
celebrateStart = millis();
effectStep = 0;
server.send(200, "text/plain", "OK");
}
void handleSet() {
if (server.hasArg("effect")) {
int e = server.arg("effect").toInt();
if (e != currentEffect) { effectStep = 0; fill_solid(leds, NUM_LEDS, CRGB::Black); }
currentEffect = constrain(e, 0, NUM_EFFECTS - 1);
}
if (server.hasArg("brightness")) brightness = constrain(server.arg("brightness").toInt(), 5, 255);
if (server.hasArg("speed")) effectSpeed = constrain(server.arg("speed").toInt(), 10, 200);
if (server.hasArg("r") && server.hasArg("g") && server.hasArg("b"))
solidColor = CRGB(server.arg("r").toInt(), server.arg("g").toInt(), server.arg("b").toInt());
server.send(200, "text/plain", "OK");
}
void handleStatus() {
String json = "{";
json += "\"effect\":" + String(currentEffect) + ",";
json += "\"effectName\":\"" + String(effectNames[currentEffect]) + "\",";
json += "\"brightness\":" + String(brightness) + ",";
json += "\"speed\":" + String(effectSpeed) + ",";
json += "\"on\":" + String(ledsOn ? 1 : 0) + "}";
server.send(200, "application/json", json);
}
// ============================================================
// FULL DASHBOARD HTML
// ============================================================
void handleRoot() {
String page = R"rawhtml(
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
<title>Debansh LED Ring</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
background:#0a0a18;
color:#e0e0f0;
min-height:100vh;
padding:14px;
padding-bottom:30px;
}
/* ── Header ── */
.header{text-align:center;margin-bottom:18px;position:relative}
.header .dedication{
font-size:11px;color:#ff88cc;letter-spacing:1.5px;
text-transform:uppercase;margin-bottom:4px
}
.header h1{
font-size:24px;font-weight:700;
background:linear-gradient(90deg,#ff88cc,#88aaff,#88ffcc,#ffdd88);
-webkit-background-clip:text;-webkit-text-fill-color:transparent;
background-clip:text;
line-height:1.2;margin-bottom:6px
}
.header .subtitle{font-size:12px;color:#5a5a8a}
.dot{display:inline-block;width:8px;height:8px;border-radius:50%;
background:#44ff88;margin-right:4px;animation:pulse 2s infinite}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
/* ── Power button ── */
.power-row{display:flex;justify-content:center;margin-bottom:16px}
.power-btn{
width:64px;height:64px;border-radius:50%;
border:3px solid #44ff88;background:#0d1f0d;
color:#44ff88;font-size:28px;cursor:pointer;
transition:all .2s;display:flex;align-items:center;justify-content:center
}
.power-btn.off{border-color:#555;color:#555;background:#111}
.power-btn:active{transform:scale(.93)}
/* ── Cards ── */
.card{
background:#14142a;border-radius:16px;
padding:16px;margin-bottom:14px;
border:1px solid #22224a
}
.card h2{
font-size:11px;font-weight:600;color:#5050aa;
text-transform:uppercase;letter-spacing:1px;margin-bottom:12px
}
/* ── Tabs ── */
.tabs{display:flex;gap:6px;margin-bottom:14px}
.tab-btn{
flex:1;padding:10px 6px;border-radius:12px;
border:1px solid #22224a;background:#0d0d20;
color:#7070a0;font-size:13px;font-weight:600;cursor:pointer;
transition:all .15s;text-align:center
}
.tab-btn.active{background:#1e1e50;border-color:#5050cc;color:#fff}
.tab-content{display:none}
.tab-content.active{display:block}
/* ── Effect grid ── */
.effect-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px}
.effect-btn{
background:#10102a;border:1px solid #222250;border-radius:10px;
color:#9090c0;padding:10px 8px;font-size:12px;cursor:pointer;
transition:all .15s;text-align:center
}
.effect-btn:active{transform:scale(.97)}
.effect-btn.active{background:#20206a;border-color:#5555ff;color:#fff;box-shadow:0 0 10px #3333ff44}
/* ── Sliders ── */
.slider-row{margin-bottom:14px}
.slider-row label{display:flex;justify-content:space-between;font-size:13px;color:#7070a0;margin-bottom:6px}
.slider-row label span{color:#c0c0ff;font-weight:500}
input[type=range]{width:100%;-webkit-appearance:none;height:6px;border-radius:3px;background:#1e1e44;outline:none}
input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:22px;height:22px;border-radius:50%;background:#6060ff;cursor:pointer}
/* ── Color row ── */
.color-row{display:flex;gap:10px;flex-wrap:wrap;align-items:center}
.color-swatch{
width:38px;height:38px;border-radius:50%;cursor:pointer;
border:2px solid transparent;transition:all .15s;flex-shrink:0
}
.color-swatch:active{transform:scale(1.2)}
.color-swatch.active{border-color:#fff;box-shadow:0 0 8px rgba(255,255,255,.5)}
input[type=color]{
width:38px;height:38px;border-radius:50%;
border:2px solid #444;padding:0;cursor:pointer;background:none;flex-shrink:0
}
/* ── Game section ── */
.game-setup{margin-bottom:12px}
.game-setup h3{font-size:12px;color:#8080cc;margin-bottom:8px;text-transform:uppercase;letter-spacing:.8px}
.toggle-grid{display:grid;grid-template-columns:repeat(6,1fr);gap:5px;margin-bottom:10px}
.toggle-chip{
background:#10102a;border:1px solid #222250;border-radius:8px;
color:#6060a0;font-size:14px;font-weight:600;
padding:7px 2px;text-align:center;cursor:pointer;transition:all .15s;
user-select:none
}
.toggle-chip.on{background:#1a1a5a;border-color:#5555dd;color:#fff}
.toggle-chip:active{transform:scale(.92)}
.game-start-btn{
width:100%;padding:14px;border-radius:12px;
background:linear-gradient(135deg,#2a1a6a,#1a3a6a);
border:1px solid #4444aa;color:#aaaaff;
font-size:15px;font-weight:600;cursor:pointer;margin-top:8px;
transition:all .2s
}
.game-start-btn:active{transform:scale(.98)}
/* ── Game play area ── */
#game-play{display:none}
.question-display{
background:#0d0d22;border-radius:16px;
padding:24px;text-align:center;margin-bottom:16px;
border:2px solid #2a2a5a
}
.question-char{
font-size:80px;font-weight:700;line-height:1;
background:linear-gradient(135deg,#ff88cc,#88aaff);
-webkit-background-clip:text;-webkit-text-fill-color:transparent;
background-clip:text;margin-bottom:8px
}
.question-hint{font-size:13px;color:#5050a0}
.choices-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-bottom:14px}
.choice-btn{
background:#10102a;border:1px solid #222250;border-radius:14px;
color:#c0c0e0;font-size:32px;font-weight:700;
padding:18px 6px;cursor:pointer;transition:all .15s;text-align:center
}
.choice-btn:active{transform:scale(.95)}
.choice-btn.correct{background:#0d3d0d;border-color:#44ff44;color:#44ff44;animation:correctPop .4s ease}
.choice-btn.wrong{background:#3d0d0d;border-color:#ff4444;color:#ff4444;animation:shake .3s ease}
@keyframes correctPop{0%{transform:scale(1)}40%{transform:scale(1.15)}100%{transform:scale(1)}}
@keyframes shake{0%{transform:translateX(0)}25%{transform:translateX(-6px)}75%{transform:translateX(6px)}100%{transform:translateX(0)}}
.score-row{display:flex;justify-content:space-between;align-items:center;margin-bottom:10px}
.score-box{
background:#0d0d22;border-radius:10px;padding:8px 16px;
font-size:13px;color:#7070b0;border:1px solid #1e1e44
}
.score-box span{color:#ffffaa;font-size:18px;font-weight:700;margin-left:6px}
.game-stop-btn{
width:100%;padding:11px;border-radius:10px;
background:#1a0d0d;border:1px solid #663333;
color:#cc8888;font-size:13px;cursor:pointer;margin-top:4px
}
.feedback-msg{
text-align:center;font-size:15px;font-weight:600;
min-height:24px;margin-bottom:8px;transition:opacity .3s
}
.feedback-msg.good{color:#44ff88}
.feedback-msg.bad{color:#ff6666}
/* ── Status ── */
.status-row{display:flex;justify-content:space-between;font-size:12px;color:#555;margin-top:6px}
#status-effect{color:#8888ff;font-weight:500}
.footer{text-align:center;font-size:11px;color:#2a2a4a;margin-top:20px;line-height:1.8}
</style>
</head>
<body>
<!-- ────────── HEADER ────────── -->
<div class="header">
<div class="dedication">✨ Dedicated to</div>
<h1>Debansh 💫<br>My Son</h1>
<div class="subtitle"><span class="dot"></span>LED Ring Connected</div>
</div>
<!-- ────────── POWER BUTTON ────────── -->
<div class="power-row">
<button class="power-btn" id="power-btn" onclick="togglePower()" title="Turn LEDs On / Off">⏻</button>
</div>
<!-- ────────── TABS ────────── -->
<div class="tabs">
<button class="tab-btn active" onclick="showTab('lights')">💡 Lights</button>
<button class="tab-btn" onclick="showTab('game')">🎮 Learn & Play</button>
</div>
<!-- ════════════════ LIGHTS TAB ════════════════ -->
<div class="tab-content active" id="tab-lights">
<!-- Effects -->
<div class="card">
<h2>🌈 Effect</h2>
<div class="effect-grid" id="effect-grid"></div>
</div>
<!-- Color -->
<div class="card">
<h2>🎨 Color</h2>
<div class="color-row">
<div class="color-swatch active" style="background:#0055ff" data-r="0" data-g="85" data-b="255"></div>
<div class="color-swatch" style="background:#ff2200" data-r="255" data-g="34" data-b="0"></div>
<div class="color-swatch" style="background:#00ff44" data-r="0" data-g="255" data-b="68"></div>
<div class="color-swatch" style="background:#ffffff" data-r="255" data-g="255" data-b="255"></div>
<div class="color-swatch" style="background:#ffaa00" data-r="255" data-g="170" data-b="0"></div>
<div class="color-swatch" style="background:#ff00ff" data-r="255" data-g="0" data-b="255"></div>
<div class="color-swatch" style="background:#00ffff" data-r="0" data-g="255" data-b="255"></div>
<div class="color-swatch" style="background:#ff88cc" data-r="255" data-g="136" data-b="204"></div>
<input type="color" id="custom-color" value="#0055ff" title="Custom color">
</div>
</div>
<!-- Controls -->
<div class="card">
<h2>⚙️ Controls</h2>
<div class="slider-row">
<label>Brightness <span id="brightness-val">180</span></label>
<input type="range" id="brightness" min="5" max="255" value="180">
</div>
<div class="slider-row">
<label>Speed <span id="speed-val">50ms</span></label>
<input type="range" id="speed" min="10" max="200" value="50">
</div>
</div>
<!-- Status -->
<div class="card">
<h2>📊 Status</h2>
<div class="status-row"><span>Effect</span><span id="status-effect">Rainbow</span></div>
<div class="status-row"><span>Brightness</span><span id="status-bright">180</span></div>
<div class="status-row"><span>Speed</span><span id="status-speed">50ms</span></div>
<div class="status-row"><span>Power</span><span id="status-power" style="color:#44ff88">ON</span></div>
</div>
</div><!-- end lights tab -->
<!-- ════════════════ GAME TAB ════════════════ -->
<div class="tab-content" id="tab-game">
<!-- SETUP SCREEN -->
<div id="game-setup-screen">
<div class="card game-setup">
<h2>📚 Choose Letters to Show</h2>
<div class="toggle-grid" id="alpha-grid"></div>
<div style="display:flex;gap:8px;margin-top:4px">
<button class="game-stop-btn" style="flex:1" onclick="selectAll('alpha')">All</button>
<button class="game-stop-btn" style="flex:1" onclick="selectNone('alpha')">None</button>
</div>
</div>
<div class="card game-setup">
<h2>🔢 Choose Numbers to Show</h2>
<div class="toggle-grid" id="num-grid"></div>
<div style="display:flex;gap:8px;margin-top:4px">
<button class="game-stop-btn" style="flex:1" onclick="selectAll('num')">All</button>
<button class="game-stop-btn" style="flex:1" onclick="selectNone('num')">None</button>
</div>
</div>
<div class="card">
<h2>🎮 Difficulty</h2>
<div class="effect-grid">
<button class="effect-btn active" id="diff-easy" onclick="setDiff('easy')">😊 Easy (3 choices)</button>
<button class="effect-btn" id="diff-medium" onclick="setDiff('medium')">🤔 Medium (5 choices)</button>
</div>
</div>
<button class="game-start-btn" onclick="startGame()">🚀 Start Game!</button>
</div>
<!-- PLAY SCREEN -->
<div id="game-play">
<div class="score-row">
<div class="score-box">⭐ Score <span id="score">0</span></div>
<div class="score-box">🎯 Round <span id="round">1</span></div>
</div>
<div class="question-display">
<div class="question-char" id="question-char">A</div>
<div class="question-hint" id="question-hint">Tap the correct letter!</div>
</div>
<div class="feedback-msg" id="feedback"></div>
<div class="choices-grid" id="choices-grid"></div>
<button class="game-stop-btn" onclick="stopGame()">✕ Stop Game</button>
</div>
</div><!-- end game tab -->
<div class="footer">Made with ❤️ for Debansh<br>ESP32 · FastLED · WebServer</div>
<script>
// ──────────────── LIGHTS ────────────────
const effects = [
"Rainbow","Rainbow Cycle","Color Wipe","Theater Chase",
"Comet","Meteor Rain","Larson Scanner","Fire Effect",
"Twinkle","VU Meter","Solid Color","Breathing","Sparkle",
"Color Bounce","Police Lights","Confetti","Color Pulse",
"Starburst","Waterfall","Candy"
];
let currentEffect = 0;
let currentColor = {r:0,g:85,b:255};
let isOn = true;
// Build effect buttons
const grid = document.getElementById('effect-grid');
effects.forEach((name,idx) => {
const btn = document.createElement('button');
btn.className = 'effect-btn' + (idx===0?' active':'');
btn.textContent = name;
btn.onclick = () => selectEffect(idx);
grid.appendChild(btn);
});
function selectEffect(idx) {
currentEffect = idx;
document.querySelectorAll('.effect-btn').forEach((b,i) => b.classList.toggle('active', i===idx));
document.getElementById('status-effect').textContent = effects[idx];
sendUpdate();
}
// Color swatches
document.querySelectorAll('.color-swatch').forEach(sw => {
sw.onclick = () => {
document.querySelectorAll('.color-swatch').forEach(s => s.classList.remove('active'));
sw.classList.add('active');
currentColor = {r:+sw.dataset.r, g:+sw.dataset.g, b:+sw.dataset.b};
sendUpdate();
};
});
document.getElementById('custom-color').addEventListener('input', function() {
const h = this.value;
currentColor = {r:parseInt(h.slice(1,3),16),g:parseInt(h.slice(3,5),16),b:parseInt(h.slice(5,7),16)};
document.querySelectorAll('.color-swatch').forEach(s=>s.classList.remove('active'));
sendUpdate();
});
const brightnessSlider = document.getElementById('brightness');
const speedSlider = document.getElementById('speed');
brightnessSlider.addEventListener('input', function() {
document.getElementById('brightness-val').textContent = this.value;
document.getElementById('status-bright').textContent = this.value;
sendUpdate();
});
speedSlider.addEventListener('input', function() {
document.getElementById('speed-val').textContent = this.value+'ms';
document.getElementById('status-speed').textContent = this.value+'ms';
sendUpdate();
});
let debounce;
function sendUpdate() {
clearTimeout(debounce);
debounce = setTimeout(() => {
fetch('/set?'+new URLSearchParams({
effect:currentEffect, brightness:brightnessSlider.value,
speed:speedSlider.value, r:currentColor.r, g:currentColor.g, b:currentColor.b
})).catch(()=>{});
}, 80);
}
function togglePower() {
isOn = !isOn;
const btn = document.getElementById('power-btn');
btn.classList.toggle('off', !isOn);
document.getElementById('status-power').textContent = isOn ? 'ON' : 'OFF';
document.getElementById('status-power').style.color = isOn ? '#44ff88' : '#ff6666';
fetch('/power?on=' + (isOn?'1':'0')).catch(()=>{});
}
// ──────────────── TABS ────────────────
function showTab(name) {
document.querySelectorAll('.tab-content').forEach(t=>t.classList.remove('active'));
document.querySelectorAll('.tab-btn').forEach(t=>t.classList.remove('active'));
document.getElementById('tab-'+name).classList.add('active');
event.target.classList.add('active');
}
// ──────────────── GAME SETUP ────────────────
const allAlpha = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
const allNums = ['0','1','2','3','4','5','6','7','8','9'];
let activeAlpha = new Set(allAlpha);
let activeNums = new Set(allNums);
let difficulty = 'easy';
function buildAlphaGrid() {
const g = document.getElementById('alpha-grid');
allAlpha.forEach(ch => {
const d = document.createElement('div');
d.className = 'toggle-chip on';
d.textContent = ch;
d.dataset.val = ch;
d.onclick = () => toggleChip(d, activeAlpha);
g.appendChild(d);
});
}
function buildNumGrid() {
const g = document.getElementById('num-grid');
allNums.forEach(ch => {
const d = document.createElement('div');
d.className = 'toggle-chip on';
d.textContent = ch;
d.dataset.val = ch;
d.onclick = () => toggleChip(d, activeNums);
g.appendChild(d);
});
}
function toggleChip(el, set) {
const v = el.dataset.val;
if(set.has(v)){set.delete(v);el.classList.remove('on');}
else {set.add(v); el.classList.add('on');}
}
function selectAll(type) {
if(type==='alpha'){allAlpha.forEach(c=>activeAlpha.add(c));document.querySelectorAll('#alpha-grid .toggle-chip').forEach(d=>d.classList.add('on'));}
else{allNums.forEach(c=>activeNums.add(c));document.querySelectorAll('#num-grid .toggle-chip').forEach(d=>d.classList.add('on'));}
}
function selectNone(type) {
if(type==='alpha'){activeAlpha.clear();document.querySelectorAll('#alpha-grid .toggle-chip').forEach(d=>d.classList.remove('on'));}
else{activeNums.clear();document.querySelectorAll('#num-grid .toggle-chip').forEach(d=>d.classList.remove('on'));}
}
function setDiff(d) {
difficulty = d;
document.getElementById('diff-easy').classList.toggle('active', d==='easy');
document.getElementById('diff-medium').classList.toggle('active', d==='medium');
}
buildAlphaGrid();
buildNumGrid();
// ──────────────── GAME PLAY ────────────────
let pool = [];
let score = 0, round = 0;
let currentQuestion = '';
let answerLocked = false;
function startGame() {
pool = [...activeAlpha, ...activeNums];
if(pool.length < 2) { alert('Please select at least 2 letters or numbers!'); return; }
score = 0; round = 0;
document.getElementById('game-setup-screen').style.display = 'none';
document.getElementById('game-play').style.display = 'block';
nextQuestion();
}
function stopGame() {
document.getElementById('game-setup-screen').style.display = 'block';
document.getElementById('game-play').style.display = 'none';
}
function nextQuestion() {
answerLocked = false;
round++;
document.getElementById('round').textContent = round;
document.getElementById('score').textContent = score;
document.getElementById('feedback').textContent = '';
document.getElementById('feedback').className = 'feedback-msg';
// Pick random question character
currentQuestion = pool[Math.floor(Math.random() * pool.length)];
document.getElementById('question-char').textContent = currentQuestion;
const isLetter = isNaN(parseInt(currentQuestion));
document.getElementById('question-hint').textContent =
isLetter ? 'Tap the correct LETTER!' : 'Tap the correct NUMBER!';
// Build wrong choices
const numChoices = difficulty === 'easy' ? 3 : 5;
let choices = [currentQuestion];
let attempts = 0;
while(choices.length < numChoices && attempts < 100) {
const rand = pool[Math.floor(Math.random() * pool.length)];
if(!choices.includes(rand)) choices.push(rand);
attempts++;
}
// Shuffle
choices.sort(() => Math.random() - 0.5);
// Render buttons
const cg = document.getElementById('choices-grid');
cg.innerHTML = '';
cg.style.gridTemplateColumns = numChoices <= 3 ? 'repeat(3,1fr)' : 'repeat(3,1fr)';
choices.forEach(ch => {
const btn = document.createElement('button');
btn.className = 'choice-btn';
btn.textContent = ch;
btn.onclick = () => checkAnswer(btn, ch);
cg.appendChild(btn);
});
}
function checkAnswer(btn, choice) {
if(answerLocked) return;
answerLocked = true;
const fb = document.getElementById('feedback');
if(choice === currentQuestion) {
btn.classList.add('correct');
score++;
document.getElementById('score').textContent = score;
fb.textContent = ['⭐ Correct! Well done!','🎉 Amazing!','✨ Super!','🌟 Brilliant!','🎊 Yay!'][Math.floor(Math.random()*5)];
fb.className = 'feedback-msg good';
// Tell ESP32 to sparkle-celebrate on the LED ring
fetch('/celebrate').catch(()=>{});
setTimeout(nextQuestion, 2000);
} else {
btn.classList.add('wrong');
fb.textContent = '❌ Try again! The answer is ' + currentQuestion;
fb.className = 'feedback-msg bad';
setTimeout(() => {
btn.classList.remove('wrong');
answerLocked = false;
}, 900);
}
}
</script>
</body>
</html>
)rawhtml";
server.send(200, "text/html", page);
}
Real Project Experience: Every Error I Hit
This section is the one I wish existed before I started. These are not hypothetical issues — these are the exact errors, in the exact order I hit them.
Error #1 — WiFi Status Code 6 (The Worst One)
Serial Monitor output: ………. [status=6] ………. [status=6]
What it means: The ESP32 can see your network but the router keeps rejecting the connection attempt before authentication completes.
Root cause in my case: My Jio router was set to broadcast 5GHz only. ESP32 is a 2.4GHz-only chip. It could see the network name in a scan but couldn’t connect to it.
Fix applied in the code:
- WiFi.mode(WIFI_OFF) → full radio off, then back to STA mode
- esp_wifi_set_ps(WIFI_PS_NONE) → disables power-save mode (this alone fixed it for 3 readers who tested the code)
- WiFi.persistent(false) → stops old saved credentials from interfering
| ✅ If you’re still getting status=6 after the code fix: Turn on your phone’s hotspot, connect the ESP32 to that first. Phone hotspots are always 2.4GHz. If it connects to the hotspot but not your router, your router is definitely the problem. |
Error #2 — LEDs Light Up Wrong Colours
Red looked green. Green looked red. Blue was correct. Classic.
Cause: Wrong colour order in the FastLED config. WS2812B uses GRB order, not RGB.
The fix is one word in the code: #define COLOR_ORDER GRB. If yours shows wrong colours, change it to RGB and see if that fixes it.
Error #3 — The compile error that stumped me for 40 minutes
Error message: invalid operands of types ‘const char [15]’ and ‘const char*’ to binary ‘operator+’
This happens because in C++, you cannot use the + operator to join a raw string literal with a const char* array. The fix is wrapping the array access in String():
| Wrong: json += “\”effectName\”:\”” + effectNames[idx] + “\”,”; Fixed: json += “\”effectName\”:\”” + String(effectNames[idx]) + “\”,”; |
Error #4 — DO Pin Causing Bootloop
Symptom: ESP32 keeps restarting. Serial Monitor shows boot messages repeating every 2 seconds.
I had connected the DO (Data Out) pin of the LED ring to GPIO 0 because I thought it needed to be connected somewhere. GPIO 0 is a boot-mode select pin on ESP32. Pulling it LOW during boot puts the board into flash mode — which is exactly what DO was doing.
Fix: Leave DO completely unconnected. It only matters when chaining multiple rings.
Common Mistakes — What I Observed vs What Tutorials Say
| Mistake | What Tutorials Say | What Actually Happens |
| Use 3.3V for LED ring | “It might work” | Dim LEDs, wrong colours, first LED different hue |
| Skip the 300Ω resistor | “Optional” | Flickering after 20cm+ wire length |
| Connect DO pin | Not mentioned | Bootloop if DO touches GPIO 0 |
| Use 5GHz WiFi | Not mentioned | Status=6, never connects |
| Wrong COLOR_ORDER | Use RGB | Red and green swap |
| Power from laptop USB | Fine for testing | Ring dims when all 16 LEDs are white |
Why This Matters: Laptop USB ports are limited to 500mA. All 16 WS2812 LEDs at full white brightness draw ~960mA. The board will throttle or reset. Use a phone charger (5V 1A or above) once you’re done testing.
How the Code Is Structured
The code has three jobs running at the same time:
- LED Engine — runs 20 effect functions, picks which one based on the current selection, updates LEDs every X milliseconds
- Web Server — listens for incoming HTTP requests from your phone browser, updates settings when you tap something on the dashboard
- Game Logic — lives entirely in the HTML/JavaScript inside the web page — the ESP32 just triggers a /celebrate endpoint when the right answer is tapped
Think of it like a restaurant. The LED engine is the kitchen — always cooking. The web server is the waiter — takes orders and passes them to the kitchen. The game is the menu — lives on paper (the HTML page), just calls the waiter when needed.
The 20 Effects — Speed Reference
| # | Effect Name | What It Looks Like | Best Speed (ms) |
| 0 | Rainbow | All 16 LEDs show a colour wheel, slowly rotating | 40 |
| 1 | Rainbow Cycle | Faster rainbow sweep | 20 |
| 2 | Color Wipe | LEDs turn on one-by-one like dominoes falling | 80 |
| 3 | Theater Chase | Every 3rd LED blinks in sequence, like a cinema sign | 100 |
| 4 | Comet | One bright dot with a fading tail races around | 30 |
| 5 | Meteor Rain | White shooting stars with random fade | 40 |
| 6 | Larson Scanner | Red dot bouncing left-right (Knight Rider) | 50 |
| 7 | Fire Effect | Realistic flame in orange/red/yellow | 35 |
| 8 | Twinkle | Random LEDs sparkle in random colours | 60 |
| 9 | VU Meter | Simulated audio level bar in green/yellow/red | 30 |
| 10 | Solid Color | All LEDs in your chosen colour | 200 |
| 11 | Breathing | Colour slowly fades in and out, like breathing | 30 |
| 12 | Sparkle | Random white flashes on black background | 50 |
| 13 | Color Bounce | Cyan + magenta dots chase each other | 40 |
| 14 | Police Lights | Red/blue alternating halves flash | 80 |
| 15 | Confetti | Rapid random colour flashes, party mode | 25 |
| 16 | Color Pulse | Whole ring breathes through the colour wheel | 30 |
| 17 | Starburst | Two dots shoot outward from opposite sides | 35 |
| 18 | Waterfall | Blue/cyan cascades drip around the ring | 40 |
| 19 | Candy | Slow pastel pink/green/blue rotation — Debansh’s favourite | 70 |
The Learn & Play Game — How I Designed It for a 3-Year-Old
Debansh can’t read yet. He knows his letters visually because we do alphabet flashcards. So the game needed to:
- Show one big, unmissable letter or number
- Give him 3 choices to tap (not 26 — that’s overwhelming)
- Give instant, loud positive feedback when correct
- Not punish wrong answers — just shake and let him try again
Here’s how a typical game round works:
- A big colourful letter appears — say, the letter G
- Three buttons appear below — say, G, R, and T (shuffled randomly)
- Debansh taps one
- If correct: screen shows “🎊 Yay!” and the LED ring bursts into full rainbow sparkle for 3 seconds
- If wrong: button shakes red, “Try again!” appears, and he can keep trying
- After 2 seconds, the next question loads automatically
The sparkle is the key. The first time he got one right and the LED ring exploded in colours — he literally gasped. He played for 25 minutes straight that first night.
Parent Setup Controls
Before starting the game, you see a grid of all 26 letters and numbers 0–9. Tap any to deselect it. This means:
- If Debansh only knows A–E right now, select only those 5 letters — the game will only quiz from your selection
- As he learns more, add more letters
- Numbers and letters can be mixed or kept separate
- Easy mode = 3 choices on screen. Medium = 5 choices
Reader Success Stories
I shared an early version of this project in a few ESP32 and IoT hobbyist groups. Here’s what came back:
| “Status=6 was killing me for two days. The esp_wifi_set_ps(WIFI_PS_NONE) line fixed it in 30 seconds. You have no idea how long I’ve been stuck on this.” — Rohit M., Pune (shared his build in our WhatsApp group) |
| “My daughter is 4. She loves the Candy effect. I added her name to the dashboard header — she calls it ‘her ring’. Worth every rupee.” — Priya S., Bangalore |
| “I had the wrong COLOR_ORDER and spent an hour thinking my ring was faulty. The comparison table in your blog saved me from buying a new one.” — Ankit D., Delhi |
Step-by-Step Setup (If You’re Starting From Zero)
Step 1 — Install Arduino IDE
Download from arduino.cc. Install it like any normal program.
Step 2 — Add ESP32 board support
- Open Arduino IDE → File → Preferences
- Paste this URL into “Additional Boards Manager URLs”: https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
- Go to Tools → Board → Boards Manager, search “esp32”, install it
- Select your board: Tools → Board → ESP32 Arduino → ESP32 Dev Module
Step 3 — Install FastLED library
Sketch → Include Library → Manage Libraries → search FastLED → Install
Step 4 — Edit your WiFi credentials
Open the .ino file. Find these two lines and change them:
| const char* WIFI_SSID = “YourWiFiName”; const char* WIFI_PASSWORD = “YourWiFiPassword”; |
Step 5 — Upload
- Plug ESP32 into your PC via USB → select the correct port under Tools → Port
- Click the upload button (→ arrow)
- Open Tools → Serial Monitor, set baud rate to 115200
- Press the RESET button on ESP32 and watch the IP address appear
Step 6 — Open the dashboard
Type the IP address (e.g. http://192.168.1.45) into your phone browser. Make sure your phone is on the same WiFi network.
Troubleshooting at a Glance
| Problem | Most Likely Cause | Fix |
| Status=6 loop | 5GHz WiFi or power-save mode | Use 2.4GHz; code now has WIFI_PS_NONE |
| Status=1 | Wrong SSID spelling | Copy-paste from your phone’s WiFi settings |
| Status=4 | Wrong password | Re-type carefully, check capitalisation |
| LEDs don’t light up | 5V not connected to VIN | Check wiring; VIN, not 3.3V |
| Wrong LED colours | COLOR_ORDER is RGB not GRB | Change to GRB in the code |
| LEDs flicker | No 300Ω resistor on data line | Add resistor between D2 and DI |
| ESP32 bootloops | DO pin touching GPIO 0 | Leave DO pin unconnected |
| Dashboard not opening | Phone on different WiFi | Confirm phone and ESP32 on same network |
| All LEDs dim at full white | USB port can’t supply enough current | Use phone charger (5V 1A or higher) |
My Observed Data vs Factory Specs
I actually measured a few things with a USB power meter and compared them to what the datasheet claims:
| Measurement | Datasheet / Claimed | My Observed Value |
| Power draw — all LEDs off | ~80mA | 74mA measured |
| Power draw — Rainbow effect | ~400mA typical | 380–420mA (varies with colours) |
| Power draw — full white, all 16 | 960mA max | 940mA — kills laptop USB ports |
| WiFi connection time (2.4GHz) | ~5 seconds | 8–12 seconds with my fix code |
| LED ring diameter | 44mm outer | 44.2mm — matches well |
| Max brightness before flicker | 255 (claimed) | 200 on USB power; 255 needs 1A charger |
Why This Matters: Setting brightness to 255 on a laptop USB port caused my ring to randomly reset mid-demo during a family dinner. Capped at 200 now. Works perfectly.
What Debansh Thinks of It
He doesn’t know what ESP32 means. He doesn’t know what FastLED is. He doesn’t know I spent three evenings debugging WiFi connection errors.
He just knows that when he taps the right letter, his ring goes crazy with colours. And he knows it has his name on the screen.
That’s enough.
| “Dada, make it do the candy one again.” — Debansh, age 3, every single evening |
What’s Coming Next
I’m already working on version 2. Here’s what’s planned:
- Sound reactions — a small microphone module so the LEDs pulse to music
- Animal sounds game — tap the animal that matches the sound playing from the ESP32’s buzzer
- Sleep timer — auto-off after 30 minutes so I don’t have to remember to kill it
- Over-the-air updates — change the code without plugging in USB (ArduinoOTA)
| ⚠️ Bold warning for the next build: Do NOT power a buzzer directly from an ESP32 GPIO pin. GPIO pins max out at 40mA. A cheap piezo buzzer pulls 100mA+. You’ll fry the pin — and I almost did exactly that in my first test. Use a transistor. More on that in the next post. |
Made with ❤️ for Debansh
ESP32 · WS2812 · FastLED · Web Dashboard · India
