Reverse-engineered TCP protocol implementation for MagicHome LED bulbs, built from first principles.
When cheap smart bulbs arrived from Alibaba without documentation or existing libraries, I decoded the proprietary protocol through packet analysis and built a complete full-stack controller. This project demonstrates network protocol reverse engineering, async TCP socket programming, color space mathematics, and production deployment—all without relying on third-party LED libraries.
Problem: MagicHome BL606A LED bulbs communicate over TCP port 5577 with an undocumented binary protocol. No Python libraries, no API specs, no documentation.
Solution: Packet capture analysis and protocol reverse engineering:
- Traffic Analysis: Captured TCP packets while using the vendor's mobile app
- Pattern Recognition: Identified command structures through hex dump analysis
- Protocol Decoding: Discovered command format:
[header][r][g][b][warm_white][mode][checksum] - Checksum Algorithm: Reverse-engineered simple sum-based checksum validation
- State Query Protocol: Decoded bidirectional status query/response mechanism
Result: Direct TCP control with <100ms latency, zero cloud dependencies, complete local network ownership.
- ✅ Async TCP Sockets - Native asyncio connection handling with 3-second timeouts
- ✅ Binary Protocol - Direct byte-level command construction (
0x31RGB,0x71power) - ✅ Bidirectional Communication - Status queries return 14-byte response packets
- ✅ Checksum Validation - Custom sum-based integrity checking
- ✅ HSV ↔ RGB Conversion - Custom implementation of color space transformations
- ✅ Hue (0-360°), Saturation (0-100%), Value (0-100%) - Accurate color representation
- ✅ Warm White Mode - Separate LED channel control for 2700K lighting
- ✅ Hex Color Support - Full color format conversion pipeline
- ✅ FastAPI Backend - REST API + WebSocket for real-time state broadcasting
- ✅ React SPA Frontend - Vite-powered TypeScript app with canvas-based color wheel
- ✅ State Management - Centralized bulb state with exponential backoff polling
- ✅ Docker Deployment - Multi-stage build with Nginx reverse proxy
- ✅ Production-Ready - SSL termination via Traefik, health monitoring, log aggregation
- Python 3.11 or higher
- Bun (recommended) or Node.js 18+
- MagicHome/BL606A LED bulbs on your local network (port 5577)
- Modern web browser with WebSocket support
-
Clone the repository
git clone https://github.com/yourusername/led-controller.git cd led-controller -
Configure your bulbs
cp config.example.json config.json # Edit config.json with your bulb IPs and names -
Install dependencies
# Frontend bun install # Backend pip install fastapi uvicorn websockets
-
Start the application
# Terminal 1 - Backend cd backend && python main.py # Terminal 2 - Frontend bun run dev
-
Open your browser
- Frontend: http://localhost:5173
- Backend API: http://localhost:8000
- API Docs: http://localhost:8000/docs
Production deployment with multi-stage builds and Nginx reverse proxy:
# Clone and configure
git clone https://github.com/yourusername/led-controller.git
cd led-controller
cp config.example.json config.json
# Edit config.json with your bulb IPs
# Build and run
docker-compose up -dContainer Architecture:
- Frontend: Vite production build served by Nginx
- Backend: FastAPI with uvicorn
- Reverse proxy: Nginx routes
/api/*→ FastAPI,/ws→ WebSocket - Process manager: Supervisor manages both services
- Port: Single container exposes port 80 (mapped to 3001 on host)
Access at: http://localhost:3001
Create a config.json file based on config.example.json:
{
"bulbs": {
"living_room": "192.168.1.100",
"bedroom": "192.168.1.101",
"kitchen": "192.168.1.102"
},
"groups": {
"all": ["living_room", "bedroom", "kitchen"],
"downstairs": ["living_room", "kitchen"],
"upstairs": ["bedroom"]
},
"colors": {
"red": "#FF0000",
"green": "#00FF00",
"blue": "#0000FF",
"warmwhite": "WW"
}
}MagicHome bulbs broadcast on your local network and listen on TCP port 5577. Find them using:
# Option 1: nmap scan
nmap -p 5577 192.168.1.0/24
# Option 2: Router admin panel
# Look for devices named "ESP_XXXXXX" or "LEDnetXXXXXX"
# Option 3: Mobile app
# Use MagicHome app to identify IPs, then switch to this controllerThe FastAPI backend provides automatic OpenAPI documentation:
- Interactive Docs: http://localhost:8000/docs
- ReDoc: http://localhost:8000/redoc
- OpenAPI Schema: http://localhost:8000/openapi.json
| Method | Endpoint | Description |
|---|---|---|
GET |
/bulbs |
Retrieve all bulb states |
GET |
/bulbs/{name} |
Get single bulb state |
POST |
/bulbs/{name}/command |
Send command to bulb (HSV, power, etc.) |
POST |
/groups/command |
Batch control multiple bulbs |
POST |
/bulbs/sync |
Force refresh all bulb states from hardware |
GET |
/groups |
List available groups |
WS |
/ws |
WebSocket for real-time state updates |
# Turn on a bulb
curl -X POST "http://localhost:8000/bulbs/lamp/command" \
-H "Content-Type: application/json" \
-d '{"action": "on"}'
# Set HSV color (native color space)
curl -X POST "http://localhost:8000/bulbs/lamp/command" \
-H "Content-Type: application/json" \
-d '{"action": "hsv", "h": 240, "s": 100, "v": 80}'
# Set hex color (converted to HSV internally)
curl -X POST "http://localhost:8000/bulbs/lamp/command" \
-H "Content-Type: application/json" \
-d '{"action": "color", "hex": "#FF5733"}'
# Warm white mode
curl -X POST "http://localhost:8000/bulbs/lamp/command" \
-H "Content-Type: application/json" \
-d '{"action": "warm_white", "brightness": 75}'
# Control multiple bulbs (group)
curl -X POST "http://localhost:8000/groups/command" \
-H "Content-Type: application/json" \
-d '{"targets": ["lamp", "dlamp"], "action": "hsv", "h": 180, "s": 100, "v": 90}'
# Force hardware sync (bypasses cache)
curl -X POST "http://localhost:8000/bulbs/sync"Connect to ws://localhost:8000/ws for real-time updates:
const ws = new WebSocket('ws://localhost:8000/ws');
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
// Initial state on connect
if (data.type === "initial_state") {
console.log(`Received ${data.data.length} bulbs`);
}
// Real-time bulb updates
if (data.type === "bulb_update") {
console.log(`Bulb ${data.data.name} changed`, data.data);
}
};Direct TCP Socket Implementation - No external LED libraries
# Example: Power on command
cmd = [0x71, 0x23, 0x0F] # header, power_on, mode
cmd.append(sum(cmd) & 0xFF) # checksum
await writer.write(bytes(cmd))
# RGB color command
cmd = [0x31, r, g, b, 0x00, 0x00, 0xF0, 0x0F]
cmd.append(sum(cmd) & 0xFF)Key Implementation Details:
- 3-second connection timeout for unreliable IoT devices
- Async context managers for proper socket cleanup
- Status query returns 14-byte response:
[header][power][mode][speed][r][g][b][ww][checksum] - Graceful offline detection (mark bulb offline vs. throwing errors)
Centralized state with smart polling:
- Background polling every 60 seconds (configurable per bulb)
- Exponential backoff for offline bulbs: 60s → 2min → 5min → 10min max
- Skip polling for recently commanded bulbs (<10s) to prevent state conflicts
- Subscriber pattern for WebSocket broadcasting
State synchronization strategy:
# Skip recent commands to avoid race conditions
if time_since_command < 10:
return # Don't poll, use cached state
# Exponential backoff for offline bulbs
if consecutive_failures > 3:
poll_interval = 600 # 10 minutesCustom HSV ↔ RGB implementation (not using colorsys):
def hsv_to_rgb(h: float, s: float, v: float) -> tuple[int, int, int]:
"""
h: 0-360 (degrees)
s: 0-100 (percentage)
v: 0-100 (percentage)
Returns: (r, g, b) as 0-255 integers
"""
# Sector-based conversion for accurate hue mapping
sector = int(h / 60) % 6
# ... mathematical transformationWhy custom implementation?
- Standard library
colorsysuses float ranges (0.0-1.0) - Direct integer RGB output (0-255) for LED commands
- Optimized for real-time color wheel interactions
Request debouncing to handle color wheel dragging:
- 100ms debounce window per bulb+action combination
- Prevents API flooding from rapid UI updates
- Allows 10 color updates/second without overwhelming hardware
WebSocket management:
- Broadcast state changes to all connected clients
- Dead connection detection and cleanup
- Ping/pong heartbeat every 30 seconds
HSV Color Wheel (src/components/color/ColorWheel.tsx):
- Canvas-based rendering for 60fps color selection
- Polar coordinates: angle = hue, radius = saturation
- Static wheel rendered once, only selection dot redrawn
- Direct HSV state (no RGB intermediate conversion)
State Flow:
User drags color wheel
↓
Update HSV state (React)
↓
Debounced API call (200ms)
↓
POST /bulbs/{name}/command
↓
BulbManager updates state
↓
WebSocket broadcast
↓
All clients update UI
Production optimizations:
- Vite code splitting for fast initial load
- Static asset caching (immutable, 1 year)
- HTML not cached (allows instant updates)
- WebSocket reconnection with exponential backoff
led-controller/
├── backend/
│ ├── led_controller.py # TCP protocol implementation (114 lines)
│ ├── bulb_manager.py # State management + polling (368 lines)
│ ├── color_utils.py # HSV/RGB math (109 lines)
│ └── main.py # FastAPI routes + WebSocket (462 lines)
├── src/
│ ├── components/
│ │ ├── color/
│ │ │ ├── ColorWheel.tsx # Canvas-based HSV picker (492 lines)
│ │ │ ├── BrightnessSlider.tsx # Value/brightness control (56 lines)
│ │ │ └── QuickColors.tsx # Preset color buttons (62 lines)
│ │ └── controls/
│ │ └── BulbControls.tsx # Main container + WebSocket (532 lines)
│ ├── App.tsx # React SPA root
│ └── main.tsx # Vite entry point
├── config.json # Bulb IPs and group definitions
├── Dockerfile # Multi-stage build (97 lines)
├── docker-compose.yml # Production orchestration
├── vite.config.js # Vite configuration
└── package.json # Bun/Node dependencies
Total codebase: ~2,300 lines (excluding dependencies)
- Backend: ~1,050 lines Python
- Frontend: ~1,140 lines TypeScript/React
- Config/Deploy: ~110 lines
# Frontend Development
bun install # Install dependencies
bun run dev # Start Vite dev server → http://localhost:5173
bun run build # Production build → dist/
bun run preview # Preview production build
bun run lint # ESLint check
# Backend Development
cd backend
python main.py # Start FastAPI → http://localhost:8000
python main.py --debug # Enable debug logging to file + console
# Docker Development
docker-compose up -d # Build and run container
docker-compose logs -f lights # Follow logs
docker-compose down # Stop and remove containerDevelopment mode (.env.development):
VITE_API_BASE=http://192.168.2.2:8000
VITE_WS_URL=ws://192.168.2.2:8000/wsProduction mode (.env.production):
VITE_API_BASE=/api
VITE_WS_URL=Vite automatically selects the correct environment. In production, Nginx proxies /api/* and /ws to the FastAPI backend.
Manual TCP test (without the full app):
import asyncio
async def test_bulb():
reader, writer = await asyncio.open_connection('192.168.1.100', 5577)
# Power on
cmd = [0x71, 0x23, 0x0F, 0xA3]
writer.write(bytes(cmd))
await writer.drain()
# Set red
cmd = [0x31, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x0F]
cmd.append(sum(cmd) & 0xFF)
writer.write(bytes(cmd))
await writer.drain()
writer.close()
asyncio.run(test_bulb())- Platform: Docker container on home server
- Reverse Proxy: Traefik with SSL (Let's Encrypt)
- Domain: lights.chanflix.com (HTTPS)
- Monitoring: Supervisor process management, log aggregation
- Network: Local network access to bulbs (no port forwarding required)
- Response time: <100ms bulb command execution
- WebSocket latency: <50ms state update broadcasts
- Concurrent users: Tested with 5 simultaneous clients
- Uptime: 30+ days (restarted only for updates)
- No authentication - Designed for private home network only
- CORS enabled - Allows cross-origin requests (local network)
- Direct IP access - Bulbs communicate on LAN only, no internet access
- SSL termination - Traefik handles HTTPS, backend runs HTTP internally
This project showcases:
-
Network Protocol Analysis
- Packet capture and reverse engineering
- Binary protocol implementation
- TCP socket programming with asyncio
-
System Design
- State management with race condition handling
- WebSocket real-time communication
- Request debouncing and optimization
-
Mathematics & Algorithms
- Color space transformations (HSV ↔ RGB)
- Polar coordinate systems for UI
- Checksum algorithms
-
Full-Stack Development
- Modern React with TypeScript
- FastAPI REST + WebSocket APIs
- Canvas-based interactive graphics
-
DevOps & Deployment
- Multi-stage Docker builds
- Nginx reverse proxy configuration
- Process management with Supervisor
- Production monitoring and logging
MIT License - see LICENSE file for details.
Built from first principles when off-the-shelf solutions didn't exist.