A feature-rich smart relay controller built on the ESP32-C3 Super Mini, featuring an animated OLED display, five-button D-pad navigation, programmable ON/OFF action chains, and a passive buzzer — all running entirely on-device with no WiFi dependency.
Think of it as a friendly little automation box for your home: run timers, set cycles (on 10 min / off 5 min), or simulate occupancy when you're away. The OLED face watches and reacts while the relay does its job.
┌──────────────────────────────────────────────────┐
│ ESP32-C3 │
│ Super Mini (USB-C) │
│ │
│ ┌─────────┐ ┌─────────────────────┐ │
│ │ Button │──── GPIO0 ┤ BACK / LEFT │ │
│ │ D-pad │──── GPIO1 ┤ UP │ │
│ │ │──── GPIO2 ┤ ENTER (active-LOW)│ │
│ │ │──── GPIO3 ┤ DOWN │ │
│ │ │──── GPIO4 ┤ MODE / RIGHT │ │
│ └─────────┘ └─────────────────────├ │
│ │
│ OLED SSD1306 (I2C) Relay Module │
│ SDA → GPIO8 SIG → GPIO20 │◄── Output
│ SCL → GPIO9 VCC / GND │
│ │
│ Buzzer USB-C Port │
│ PWM → GPIO21 CDC Serial │
└──────────────────────────────────────────────────┘
| Feature | Details |
|---|---|
| Processor | ESP32-C3 (RISC-V), USB-C native Serial CDC, 8 MHz crystal |
| Display | 128x64 SSD1306 OLED via I2C, animated at ~60 FPS |
| Relay | 5V single-channel module — up to 10A / 250VAC (configurable) |
| Buttons | 5× momentary D-pad (UP / DOWN / ENTER / LEFT / MODE) |
| Buzzer | Passive piezo with multi-tone cues via LEDC PWM |
| Storage | NVS flash — up to 8 programmable ON/OFF chains |
| Modes | Run Chain · Countdown Timer · Manual Mode · Occupancy Simulation |
The OLED is not a static display — it features a full animation engine:
- "RoboEyes" idle face — Two animated blocks that scan the room, blink, and express mood
- Spring-physics selection highlights that slide and settle naturally
- Slide transitions between screens with eased motion
- Progress rings, pulsing power glyphs, and celebration animations on chain completion
- Emergency stop screen with angry eyes and flashing border
- All motion routes through easing curves — zero linear movement
- Run Chain — Select a saved ON/OFF sequence and run it (once, twice, 3×, 5×, 10×, or forever)
- Countdown Timer — Set the relay ON or OFF for N minutes, then auto-stop
- Manual Mode — Hold UP/DOWN to toggle the relay; release to stop
- Occupancy Simulation — Random ON/OFF cycles between min/max intervals to make it look like someone's home
- Chain Builder — Create/edit your own multi-step automation sequences
- Settings — Buzzer on/off, volume, brightness, screen sleep timeout
| # | Component | Notes |
|---|---|---|
| 1 | ESP32-C3 Super Mini | USB-C module; Dev Module variant (8 MHz crystal) |
| 2 | SSD1306 OLED Module | 128×64 I2C, GND/VCC/SCL/SDA (no reset pin needed) |
| 3 | 5V Relay Module | Single-channel; SIG data pin to GPIO20 |
| 4 | Passive Buzzer | Piezo element; PWM-driven via GPIO21 |
| 5 | Push Buttons × 5 | Momentary, 6×6 mm standard (or equivalent) |
| 6 | Male Header Pins × 3 | For relay SIG, buzzer V/G, and load output (COM/NO) |
Notes:
- [10k] = 10 kΩ pull-down resistor on each active-HIGH button input
- [BTN] = momentary push button, one side to GPIO, other to GND
- BUTTON_ENTER (GPIO2) is the ONLY active-LOW button (strapping pin constraint)
- Relay module: SIG=active-LOW trigger; wire SIG→GPIO20, VCC→5V, GND→GND
- Buzzer: + lead → GPIO21 via small series resistor (~100Ω), - lead → GND
- Relay NO/COM wired in the hot line between mains live and the socket
SSD1306 OLED (0x3C)
SDA ──→ GPIO8
SCL ──→ GPIO9
VCC ──→ 3.3V
GND ──→ GND
┌──────────────────────────────────────┐
│ ESP32-C3 Module │
│ [USB-C] [GPIO pins down] │
│ │
│ OLED on top, facing user │
│ Buttons accessible from front │
│ Relay module mounted behind │
│ Buzzer peeking through case │
└──────────────────────────────────────┘
- Arduino CLI installed (or the Arduino IDE) with ESP32 board support
- ESP32 board package:
esp32version 3.3.10 (or newer) targetingesp32:esp32c3 - U8g2 library (by Oliver Ertl) — version from the Arduino Library Manager
1. Install ESP32 boards via Boards Manager:
→ Go to File > Preferences
→ Board Manager URLs:
https://raw.githubusercontent.com/espressif/arduino-esp32/master/package_esp32_index.json
→ Search "ESP32 by espressif" and install (v3.x+)
2. Install U8g2 library:
→ Go to Sketch > Include Library > Manage Libraries
→ Search "U8g2" and install
3. Select board:
→ Tools > Board > ESP32 Boards > ESP32-C3 Super Mini
→ Flash Method: Download Mode (or Native USB)
→ CDC on Boot: No CDC (since we use the native CDC ourselves)
4. Open the project:
→ File > Open > Navigate to SmartSocket/SmartSocket.ino
5. Compile and upload via USB-C.
# Install ESP32 core
arduino-cli core install esp32:esp32 --additional-urls \
https://raw.githubusercontent.com/espressif/arduino-esp32/master/package_esp32_index.json
# Compile
arduino-cli compile \
--fqbn esp32:esp32:esp32c3 \
--build-path build \
"SmartSocket/SmartSocket.ino"
# Upload
arduino-cli upload \
--fqbn esp32:esp32:esp32c3 \
-p /dev/cu.usbserial-XXXX \
"SmartSocket/SmartSocket.ino"
# Monitor serial output (115200 baud)
arduino-cli monitor \
-p /dev/cu.usbserial-XXXX \
-c baudrate=115200After flashing, open the Serial Monitor at 115200 baud. You should see:
[I] ==== Smart Socket boot ====
[I] log level 3
[I] UP pin 1 active-HIGH
[I] DOWN pin 3 active-HIGH
[I] BACK pin 0 active-HIGH
[I] ENTER pin 2 active-LOW
[I] MODE pin 4 active-HIGH
[I] buttons init: ignore window 600ms
The OLED should come alive showing the idle face — two curious "eyes" scanning around looking for people. Press any button to enter the menu.
SmartSocket/
├── SmartSocket.ino # Sketch entry point (just includes src/)
├── README.md # You are here
└── src/
├── config.h # All hardware constants, pin map, enums, logging macros
├── SmartSocket.ino # setup() and loop() — wire everything together
├── buttons.h / .cpp # 5-button D-pad with debouncing, hold detection, auto-repeat
├── menu.h / .cpp # Screen state machine: 20+ screens, full navigation logic
├── oled.h / .cpp # SSD1306 rendering + animation engine (RoboEyes, springs)
├── timer.h / .cpp # Chain execution engine with NVS power-loss resume
├── chain.h / .cpp # NVS storage for up to 8 ActionChain blobs
├── settings.h / .cpp # Device preferences persisted via Preferences/NVS
├── relay.h / .cpp # Relay controller with safety timing floors
└── buzzer.h / .cpp # Passive buzzer: multi-tone pattern queue via LEDC PWM
| File | Responsibility |
|---|---|
config.h |
Single source of truth — pin map, timing constants, all enums (MenuScreen, ButtonId, ChainStep), per-button polarity table |
menu.cpp |
20+ screen state machine — the central brain. Processes button events, dispatches to per-screen handlers, calls render methods each frame |
oled.cpp |
Animation engine — RoboEyes face (searching/happy/angry/sleepy), spring-physics highlights, slide transitions, progress rings, power glyphs |
timer.cpp |
Chain executor — runs saved sequences against the relay, handles step timing, cycle counting, NVS snapshot for power-loss resume |
chain.cpp |
NVS persistence — saves/loads ActionChain blobs to/from flash across reboots |
buttons.cpp |
Debounce + hold detection — per-button active-high/low polarity, startup-ignore window, auto-repeat ramp for UP/DOWN |
UP / DOWN → Navigate lists or adjust values
ENTER → Select / confirm / enter edit mode
BACK (LEFT) → Go back / cancel
MODE (RIGHT)→ Secondary action / next field / change value
Press ENTER or MODE from the idle face to open the home menu:
┌──────────────────────────┐
│ Smart Socket │
│ ──────────────────────── │
│ ▸ Run Chain │ ← Press and run a saved sequence
│ Timer │ ← One-shot countdown (ON or OFF for N min)
│ Manual Mode │ ← Hold UP to turn on, DOWN to turn off
│ Presence │ ← Random ON/OFF to simulate occupancy
│ New Chain │ ← Create a custom automation sequence
│ Settings │ ← Buzzer, brightness, sleep timeout
└──────────────────────────┘
- Select Run Chain → ENTER
- Pick a chain from the list (UP/DOWN) → ENTER
- Watch it run! The OLED shows a progress ring around a pulsing lightning bolt
- Press BACK to stop, or MODE to pause/resume
- Select New Chain → ENTER
- Name it: use UP/DOWN to cycle characters, RIGHT to move to the next slot
- The chain builder shows each step; ENTER on a step to edit its ON/OFF state and duration
- Use + Add Step to insert new steps, or DELETE (BACK on a step) to remove
- Adjust Repeat count: Once / 2× / 3× / 5× / 10× / Forever
- Press Save Chain → ENTER to store it in flash
- Select Timer → ENTER
- Set target: ON or OFF (toggle the chip)
- Set duration: 1–240 minutes (UP/DOWN, RIGHT to advance field)
- Press Start → The relay activates for N minutes, then flips off and completes
- Select Presence → ENTER
- Set min/max interval in minutes (the randomness window)
- Press Start — The relay alternates ON/OFF randomly between those bounds, forever
- Great for making your home look lived-in while you're away
Long-press ENTER from idle to enter manual mode:
- Hold UP → Relay turns ON (hold to keep it on)
- Hold DOWN → Relay turns OFF (hold to keep it off)
- Press BACK to exit
Emergency Stop: Hold ENTER for 3 seconds during any run or manual mode — the relay is forcibly turned off immediately and an angry red warning screen flashes.
| Setting | What It Does |
|---|---|
| Buzzer: ON/OFF | Toggle haptic audio feedback on/off |
| Volume: 0-100% | Adjust buzzer PWM duty cycle (mapped to perceived loudness) |
| Bright: 0-100% | OLED contrast — maps to SSD1306 internal contrast register |
| Sleep: 15s–5min | Screen inactivity timeout before the OLED dims; 0 = never |
| Delete Chain | Pick and erase a saved chain from flash |
All hardware constants live in one file. Key things you might change:
// Pin map (modify these if your wiring differs)
constexpr uint8_t PIN_OLED_SDA = 8;
constexpr uint8_t PIN_OLED_SCL = 9;
constexpr uint8_t PIN_RELAY = 20;
constexpr uint8_t PIN_BUZZER = 21;
// Button polarity table (index = ButtonId)
constexpr bool BUTTON_ACTIVE_HIGH[BUTTON_COUNT] = {
true, // UP — GPIO1, wired to 3V3, pull-down idle
true, // DOWN — GPIO3, wired to 3V3, pull-down idle
true, // BACK — GPIO0, wired to 3V3, pull-down idle
false, // ENTER — GPIO2 (strapping pin! active-LOW, wired to GND)
true, // MODE — GPIO4, wired to 3V3, pull-down idle
};
// Timing constants
constexpr uint32_t RELAY_MIN_ON_MS = 30000; // Min relay ON duration (30s safety floor)
constexpr uint32_t RELAY_MIN_OFF_MS = 5000; // Min relay OFF duration (5s safety floor)
// Logging
#define LOG_LEVEL 3 // 0=off, 1=errors, 2=info, 3=verbose per-frame
⚠️ Important: GPIO2 is a strapping pin on ESP32-C3. It idles HIGH by default and cannot be reliably used as an active-HIGH input. The ENTER button must be wired to GND and treated as active-LOW. Wiring it incorrectly will cause the device to auto-navigate into manual mode at boot.
The device uses the USB-C native Serial CDC port — connect your computer via USB and open a serial monitor at 115200 baud for full boot logs including button states, chain loads, and menu navigation events.
| Problem | Likely Cause | Fix |
|---|---|---|
| OLED stays black at boot | I2C address wrong or loose wiring | Check SDA/SCL connections; ensure address is 0x3C (not 0x3D) |
| Device jumps into Manual Mode on boot | GPIO2 floating HIGH during strapping | Verify ENTER button is wired to GND, not 3V3; check pull-up is active |
| Buttons do nothing after reboot | Startup ignore window triggered by noise | The firmware suppresses input for 600ms at boot — wait and try again |
| Relay clicks but won't stay on | RELAY_MIN_ON_MS safety floor violated |
The chain tries to switch too fast; increase step durations in the builder |
| NVS chains fail to load after code change | ActionChain struct size changed |
Adding/removing fields invalidates saved chains — re-seed from scratch |
| Serial prints nothing | Wrong baud rate or no CDC init | Try 115200; ensure Serial.begin() called before other prints |
| OLED flickers / garbled pixels | I2C pull-ups missing or too weak | Add 4.7 kΩ pull-ups on SDA and SCL lines |
| Buzzer is silent despite settings | PWM duty mapped to 0 | Check buzzer volume setting; verify PIN_BUZZER wiring |
Set LOG_LEVEL = 3 in config.h and reflash. This enables per-frame logging of button states, menu transitions, chain execution details, and relay timing events.
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Buttons │────▶│ Menu │────▶│ OLED │
│ (Input) │ │ (State) │ │ (Output)│
└──────────┘ └────┬─────┘ └──────────┘
│
┌──────┴──────┐
│ Timer / │
│ Chain │
│ Engine │
└──────┬──────┘
│
┌────┴─────┐
│ Relay / │
│ Buzzer │
│ (Actuators)│
└──────────┘
Data Flow: Button Event → Screen State Machine → OLED Render + Relay Actuation
The firmware follows a clean Input → State → Output pattern with all subsystems wired as global singletons. The animation system drives the OLED at up to ~60 FPS using spring physics and easing curves — every pixel movement routes through an easing curve, never linearly.
This project is provided as-is for educational and personal use. No warranty expressed or implied.
Built with love for small embedded projects. Every screen transition, every button press, every relay click — all running from a single ESP32-C3 Super Mini.



