Production-Grade ESP8266 IoT Smart Relay Controller
1–4 Channel • Bilingual AR/EN • MQTT + Home Assistant • PWA • OTA Updates
- Overview
- Features
- Hardware Requirements
- Wiring Diagram
- Getting Started
- Usage Guide
- API Reference
- Architecture
- Extending the Firmware
- Configuration Files
- Performance
- Contributing
- License
- Contact
Elmahdy Relay is a production-grade IoT smart relay controller built on the ESP8266 (ESP-12F / NodeMCU) platform. It provides a beautiful, fully bilingual (Arabic RTL / English LTR) web dashboard for controlling 1–4 relay channels over WiFi, with real-time WebSocket updates, MQTT integration for smart home systems like Home Assistant, countdown and scheduled timers, scene presets, and much more — all packed into a single flashable .bin file.
Why Elmahdy Relay?
- 🏭 Production-ready — Corruption-safe config storage, dual-bank OTA, hardware watchdog
- 🌐 Fully local — No cloud dependency. Works on your local network, even without internet
- 🇸🇦 Arabic-first — Full RTL Arabic UI with instant language switching to English
- 📱 PWA installable — Add to home screen for an app-like experience
- 🏠 Home Assistant ready — MQTT auto-discovery, zero config needed
- ⚡ Blazing fast — Boot in <5s, UI loads in <2s, relay toggle in <100ms
- 1–4 configurable channels with independent GPIO pin assignment
- Power-on state per channel: Last State, Always ON, or Always OFF
- Interlock groups — Only one relay ON at a time (safety for motors/curtains)
- Pulse/Inching mode — Auto-OFF after configurable duration (garage doors, gates)
- Staggered activation — 50ms delay between relays in bulk commands to prevent electrical surge
- All ON / All OFF bulk control
- Boot-sensitive GPIO warning — Warns if GPIO 0, 2, or 15 is selected
- Countdown timers — "Turn OFF after 30 minutes"
- Scheduled timers — "Turn ON at 18:00 daily"
- Repeat modes: Once, Daily, Weekdays, Weekend, Custom days
- NTP time sync with configurable timezone
- Power-cycle safe — Timers persist and resume after reboot
- Max 8 timers total (countdown + scheduled)
- Named presets grouping multiple relay states (e.g., "Night Mode")
- Arabic & English names supported
- One-tap activation from dashboard, MQTT, or PWA
- Max 10 scenes
- Full MQTT integration with configurable broker, port, auth, and topic prefix
- Home Assistant Auto-Discovery — Devices appear automatically in HA
- Last Will & Testament (LWT) for online/offline status detection
- Per-channel control & status topics
- Scene activation via MQTT
- System info topic (firmware version, uptime, RSSI, IP)
- Exponential backoff reconnect — Never blocks local control
- Full Arabic (RTL) and English (LTR) with instant switching
- No page reload — Language toggle is instant
- External language packs — Easy to add new languages
- Persistent preference — Remembers your choice across reboots
- Add to Home Screen on Android & iOS
- Standalone mode — Opens without browser chrome
- Service worker caches static assets for offline local network access
- Web UI firmware upload — No USB cable needed after initial flash
- File size validation before flashing
- Progress bar during update
- Dual-bank protection — Power loss during OTA? Previous firmware is preserved
- Config preserved across firmware updates
- One-click backup downloads all device config as a single file
- Restore to clone settings across devices or recover after factory reset
- Buzzer — Beep on toggle, double-beep on WiFi connect, long beep on factory reset
- LED status — Fast blink (AP only), slow blink (WiFi connected), solid (MQTT connected)
- Physical reset button — Short press: toggle relay 1, long press: reboot, very long press: factory reset
- Anti-spam protection — 100ms minimum between buzzer activations
- Graceful degradation — Device works fine without buzzer/LED/button hardware
- Corruption-safe writes — Write to temp → CRC32 verify → atomic rename
- Per-section factory defaults — Only the corrupted config section resets
- Hardware watchdog — Auto-recovery from firmware hangs
- AP always active — Never lose access to your device
- No
delay()calls — Fully non-blocking firmware
| Component | Specification | Notes |
|---|---|---|
| MCU | ESP8266 (ESP-12F) or NodeMCU v2 | 4MB flash required |
| Relay Module | 1–4 channel relay board | Active-LOW (default) |
| Power Supply | 5V DC, ≥1A | Powers both MCU and relays |
| Buzzer (optional) | Active buzzer, 3.3V | Connected to GPIO13 (D7) |
| Reset Button (optional) | Momentary push button | Connected to GPIO16 (D0) |
| Function | GPIO | NodeMCU Pin | Notes |
|---|---|---|---|
| Relay 1 | GPIO5 | D1 | ✅ Boot-safe |
| Relay 2 | GPIO4 | D2 | ✅ Boot-safe |
| Relay 3 | GPIO14 | D5 | ✅ Boot-safe |
| Relay 4 | GPIO12 | D6 | ✅ Boot-safe |
| Buzzer | GPIO13 | D7 | Active buzzer |
| Reset Button | GPIO16 | D0 | Pull-up to 3.3V |
| Status LED | GPIO2 | D4 | Built-in LED (active-LOW) |
Important
GPIO 0, 2, and 15 are boot-sensitive. Avoid using them for relays unless you understand the implications. The firmware will warn you if you try.
- PlatformIO — Install PlatformIO IDE (VS Code extension) or PlatformIO Core (CLI)
- Python
intelhexpackage — Required for binary merge (one-time setup):%USERPROFILE%\.platformio\penv\Scripts\pip.exe install intelhex
- Tasmotizer (optional) — GUI tool for flashing: Download Tasmotizer
Clone this repository and build:
# Clone the repo
git clone https://github.com/Sayedelmahdy/Elmahdy-Relay.git
cd Elmahdy-Relay/firmware
# Build firmware
pio run
# Build LittleFS filesystem image (web UI + language files)
pio run --target buildfsOr use the included build script (Windows PowerShell):
cd firmware
.\build.ps1- Build the merged binary:
# From the firmware/ directory pio run && pio run --target buildfs # Merge into single .bin python -m esptool --chip esp8266 merge_bin \ --flash_mode dio --flash_freq 80m --flash_size 4MB \ -o elmahdy-relay-full.bin \ 0x000000 .pio/build/nodemcuv2/firmware.bin \ 0x200000 .pio/build/nodemcuv2/littlefs.bin
- Open Tasmotizer
- Select your COM port
- Browse to
elmahdy-relay-full.bin - ✅ Check "Erase before flashing" (first time only)
- Click "Tasmotize!"
# Erase flash (first time only)
pio run --target erase --upload-port COM3
# Flash firmware + filesystem
pio run --target upload --upload-port COM3
pio run --target uploadfs --upload-port COM3Navigate to System Settings → Firmware Update in the web UI and upload the .bin file.
- Power on the device
- Connect your phone/laptop to WiFi network:
ElmahdyRelay_XXXX(password:12345678) - A captive portal will redirect you to
http://192.168.4.1 - The setup page loads in Arabic by default
- Tap "بحث عن الشبكات" (Scan Networks) to find your home WiFi
- Select your network, enter the password, and save
- The device connects to your home WiFi while keeping the AP active
- Access the dashboard via the device's new IP (shown after connecting) or via
http://192.168.4.1
Tip
The AP is always active — you can always access the device at 192.168.4.1 even if your home WiFi goes down.
The main dashboard shows:
- Relay cards with toggle switches for each channel
- All ON / All OFF bulk control buttons
- Scene preset quick-activate buttons
- System info: WiFi signal (RSSI bars), uptime, MQTT status, firmware version
- Language toggle (AR/EN) in the header
All state changes are real-time via WebSocket — no page refresh needed. Multiple users can control the device simultaneously.
Navigate to Configuration → Relay Settings:
- Set number of active channels (1–4)
- Assign GPIO pin per channel
- Set channel name (up to 20 characters, Arabic or English)
- Configure power-on state: Last State / Always ON / Always OFF
- Enable pulse mode with configurable duration
- Set up interlock groups (mutually exclusive relays)
Navigate to Configuration → Timers:
- Select a channel and target state (ON/OFF)
- Set duration (minutes)
- Timer persists across reboots
- Select a channel and target state
- Set the time (HH:MM)
- Choose repeat mode: Once / Daily / Weekdays / Weekend / Custom days
- Requires NTP time sync (pauses if NTP is unavailable)
Navigate to Configuration → Scenes:
- Create named scenes with specific relay states per channel
- Names support Arabic and English
- Trigger scenes from the dashboard, MQTT, or PWA
- Maximum 10 scenes
Navigate to Configuration → MQTT:
| Setting | Default | Description |
|---|---|---|
| Broker | broker.hivemq.com |
MQTT broker address |
| Port | 1883 |
Broker port |
| Username | (empty) | Auth username |
| Password | (empty) | Auth password |
| Prefix | elmahdy |
Topic prefix |
When MQTT is enabled, the device automatically publishes HA discovery messages on boot. Your relays appear in Home Assistant with zero additional configuration.
- Tap the AR/EN button in the header to switch languages
- Arabic activates full RTL layout
- English activates LTR layout
- Switch is instant — no page reload
- Preference persists across sessions and reboots
- Go to System Settings → Firmware Update
- Click Browse and select the new
.binfile - The firmware validates the file size
- A progress bar shows during flashing
- Device auto-reboots after success
- All your settings are preserved
Note
If power is lost during OTA, the dual-bank protection preserves the previous working firmware.
- Backup: System Settings → Download Backup (exports all config as a single file)
- Restore: System Settings → Upload Restore file → Device reboots with restored settings
Useful for cloning settings across multiple devices.
On Android Chrome or iOS Safari:
- Open the dashboard URL
- Browser prompts "Add to Home Screen"
- The app installs and opens in standalone mode (no browser chrome)
- Cached assets work on local network even without internet
All endpoints support CORS for external application access.
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/status |
Full device status (relays, WiFi, MQTT, system) |
GET |
/api/relays |
Relay states for all channels |
POST |
/api/relay/{ch} |
Toggle relay (body: {"state": "ON"/"OFF"/"TOGGLE"}) |
POST |
/api/relay/all |
All ON/OFF (body: {"state": "ON"/"OFF"}) |
GET |
/api/timers |
List all timers |
POST |
/api/timer |
Create a timer |
DELETE |
/api/timer/{id} |
Delete a timer |
GET |
/api/scenes |
List all scenes |
POST |
/api/scene |
Create/update a scene |
POST |
/api/scene/{name}/activate |
Activate a scene |
DELETE |
/api/scene/{name} |
Delete a scene |
GET |
/api/config/wifi |
Get WiFi configuration |
POST |
/api/config/wifi |
Save WiFi configuration |
GET |
/api/config/mqtt |
Get MQTT configuration |
POST |
/api/config/mqtt |
Save MQTT configuration |
GET |
/api/config/relays |
Get relay configuration |
POST |
/api/config/relays |
Save relay configuration |
GET |
/api/config/system |
Get system configuration |
POST |
/api/config/system |
Save system configuration |
GET |
/api/scan |
Scan WiFi networks |
POST |
/api/ota |
Upload firmware (multipart) |
GET |
/api/backup |
Download full config backup |
POST |
/api/restore |
Upload and restore config backup |
POST |
/api/factory-reset |
Factory reset the device |
POST |
/api/reboot |
Reboot the device |
Connect to ws://<device-ip>/ws for real-time bidirectional communication.
Server → Client Messages:
{"type": "relay_state", "channel": 1, "state": "ON"}
{"type": "all_state", "relays": [{"ch": 1, "state": "ON"}, {"ch": 2, "state": "OFF"}]}
{"type": "timer_update", "timers": [...]}
{"type": "system_info", "uptime": 3600, "rssi": -45, "mqtt": true}Client → Server Commands:
{"action": "toggle", "channel": 1}
{"action": "set", "channel": 1, "state": "ON"}
{"action": "all_on"}
{"action": "all_off"}
{"action": "scene", "name": "night"}Default prefix: elmahdy
| Topic | Direction | Payload | Description |
|---|---|---|---|
{prefix}/relay/{ch}/control |
Subscribe | ON / OFF / TOGGLE |
Control a relay |
{prefix}/relay/{ch}/status |
Publish | ON / OFF |
Relay state feedback |
{prefix}/scene/{name}/control |
Subscribe | ON |
Activate a scene |
{prefix}/status |
Publish | online / offline |
LWT availability |
{prefix}/system |
Publish | JSON | System info (version, uptime, RSSI, IP) |
homeassistant/switch/... |
Publish | JSON | HA auto-discovery config |
┌─────────────────────────────────────────────────────────┐
│ ESP8266 Firmware │
│ │
│ ┌─────────────┐ ┌──────────────┐ ┌───────────────┐ │
│ │ WiFi Manager│ │ Web Server │ │ MQTT Manager │ │
│ │ (AP+STA) │ │(ESPAsyncWeb) │ │(AsyncMqttCli) │ │
│ └──────┬──────┘ └──────┬───────┘ └──────┬────────┘ │
│ │ │ │ │
│ │ ┌──────┴───────┐ │ │
│ │ │ WebSocket │ │ │
│ │ │ Handler │ │ │
│ │ └──────┬───────┘ │ │
│ │ │ │ │
│ ┌──────┴────────────────┴──────────────────┴────────┐ │
│ │ Relay Controller │ │
│ │ (GPIO control, interlock, pulse, state) │ │
│ └──────┬──────────────────────────────┬─────────────┘ │
│ │ │ │
│ ┌──────┴──────┐ ┌────────┴────────┐ │
│ │Timer Engine │ │ Scene Manager │ │
│ │(countdown + │ │ (presets, CRUD) │ │
│ │ scheduled) │ └─────────────────┘ │
│ └─────────────┘ │
│ │
│ ┌─────────────┐ ┌──────────────┐ ┌───────────────┐ │
│ │ Config │ │ Language │ │ Buzzer / │ │
│ │ Manager │ │ Manager │ │ LED / Reset │ │
│ │ (LittleFS) │ │ (AR/EN packs)│ │ Controllers │ │
│ └─────────────┘ └──────────────┘ └───────────────┘ │
└─────────────────────────────────────────────────────────┘
| Module | File | Responsibility |
|---|---|---|
| WiFi Manager | wifi_manager.h/.cpp |
AP+STA mode, captive portal, WiFi scanning |
| Web Server | web_server.h/.cpp |
HTTP routes, static file serving, CORS |
| WebSocket Handler | websocket_handler.h/.cpp |
Real-time bidirectional communication |
| MQTT Manager | mqtt_manager.h/.cpp |
Broker connection, HA discovery, LWT |
| Relay Controller | relay_controller.h/.cpp |
GPIO control, interlock, pulse, state |
| Timer Engine | timer_engine.h/.cpp |
Countdown + scheduled timers, NTP sync |
| Scene Manager | scene_manager.h/.cpp |
Scene CRUD, activation |
| Config Manager | config_manager.h/.cpp |
LittleFS JSON storage, CRC32, atomic writes |
| Language Manager | language_manager.h/.cpp |
Language pack loading, string lookup |
| Buzzer Controller | buzzer_controller.h/.cpp |
Beep patterns, anti-spam |
| LED Controller | led_controller.h/.cpp |
Status LED patterns |
| Reset Handler | reset_handler.h/.cpp |
Button press duration detection |
This firmware is designed to be modular. Here's how to extend it:
- Create the module — Add
sensor_name.handsensor_name.cppinfirmware/src/ - Follow the pattern — Look at existing modules like
buzzer_controller.h/.cppfor structure:// sensor_name.h #ifndef SENSOR_NAME_H_ #define SENSOR_NAME_H_ namespace SensorName { void begin(); // Called once in setup() void loop(); // Called every loop() iteration float readValue(); // Your sensor-specific function } #endif
- Register in
main.cpp— Add#include "sensor_name.h"and callSensorName::begin()insetup(),SensorName::loop()inloop() - Add API endpoint — In
web_server.cpp, add a route:server.on("/api/sensor", HTTP_GET, [](AsyncWebServerRequest *request) { // Return sensor data as JSON });
- Add WebSocket updates — In
websocket_handler.cpp, broadcast sensor data - Add MQTT topic — In
mqtt_manager.cpp, publish sensor readings - Update the UI — Add a card in
index.htmland updateapp.js
In web_server.cpp:
server.on("/api/your-endpoint", HTTP_GET, [](AsyncWebServerRequest *request) {
JsonDocument doc;
doc["key"] = "value";
String response;
serializeJson(doc, response);
request->send(200, "application/json", response);
});- Copy
firmware/data/lang_en.jsontofirmware/data/lang_xx.json - Translate all string values
- Add the language option in
language_manager.cpp - Update the UI toggle in
index.html
All configuration is stored in LittleFS as separate JSON files with corruption-safe write patterns:
| File | Contents |
|---|---|
wifi.json |
SSID, password, static IP settings, AP password |
mqtt.json |
Broker, port, auth, prefix, enabled state |
relays.json |
Channel count, GPIO pins, names, power-on states, pulse, interlock |
timers.json |
All countdown and scheduled timers |
scenes.json |
All scene presets |
system.json |
Buzzer/LED/reset settings, hostname, language, timezone |
Note
Write safety: All config writes use a write-to-temp → CRC32 verify → atomic rename pattern. If power is lost during a write, the previous valid config remains intact.
| Metric | Target | Achieved |
|---|---|---|
| Boot to functional state | < 5 seconds | ✅ |
| Web UI load time | < 2 seconds | ✅ |
| Relay toggle latency | < 100ms | ✅ |
| WebSocket state push | < 50ms | ✅ |
| Firmware binary size | < 500KB | ✅ |
| Web assets + config | < 512KB | ✅ |
| Config persistence | 100+ power cycles | ✅ |
| Concurrent WebSocket clients | 4 | ✅ |
Contributions are welcome! Here's how to get started:
- Fork this repository
- Create a feature branch:
git checkout -b feature/my-new-sensor - Follow the code style — Look at existing modules for patterns
- Test on hardware — This is embedded firmware, always test on real ESP8266
- Keep it lightweight — Remember the 500KB firmware + 512KB filesystem budget
- Submit a Pull Request with a clear description
- Use
monitor_speed = 115200for serial debugging - Use
monitor_filters = esp8266_exception_decoderfor crash stack traces - The firmware uses no
delay()calls — keep all code non-blocking - All web assets use vanilla ES5 JavaScript — no frameworks, no transpilers
- Compress web assets with gzip for LittleFS deployment
- 🌡️ Sensor integrations (DHT22, BMP280, current sensors, etc.)
- 🌐 Additional languages (French, Turkish, Urdu, etc.)
- 📊 Energy monitoring features
- 🔐 Optional authentication for the web UI
- 📱 Mobile app (React Native / Flutter)
- 🧪 Automated testing framework
This project is licensed under the MIT License — see the LICENSE file for details.
Sayed Elmahdy
- 📱 Phone: 01093307397
- 🐙 GitHub: @Sayedelmahdy
Made with ❤️ by Sayed Elmahdy
© 2026 Elmahdy Relay — All rights reserved
