Repository files navigation

LED Controller

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.

LED Controller InterfacePython 3.11+ReactFastAPI

The Challenge: Reverse Engineering a Proprietary Protocol

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:

  1. Traffic Analysis: Captured TCP packets while using the vendor's mobile app
  2. Pattern Recognition: Identified command structures through hex dump analysis
  3. Protocol Decoding: Discovered command format: [header][r][g][b][warm_white][mode][checksum]
  4. Checksum Algorithm: Reverse-engineered simple sum-based checksum validation
  5. State Query Protocol: Decoded bidirectional status query/response mechanism

Result: Direct TCP control with <100ms latency, zero cloud dependencies, complete local network ownership.

Technical Highlights

Protocol Implementation

  • Async TCP Sockets - Native asyncio connection handling with 3-second timeouts
  • Binary Protocol - Direct byte-level command construction (0x31 RGB, 0x71 power)
  • Bidirectional Communication - Status queries return 14-byte response packets
  • Checksum Validation - Custom sum-based integrity checking

Color Space Mathematics

  • 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

System Architecture

  • 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

Quick Start

Prerequisites

  • 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

Installation

  1. Clone the repository

    git clone https://github.com/yourusername/led-controller.git
    cd led-controller
  2. Configure your bulbs

    cp config.example.json config.json
    # Edit config.json with your bulb IPs and names
  3. Install dependencies

    # Frontend
    bun install
    # Backend
    pip install fastapi uvicorn websockets
  4. Start the application

    # Terminal 1 - Backendcd backend && python main.py
    # Terminal 2 - Frontend
    bun run dev
  5. Open your browser

Docker Deployment (Recommended)

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 -d

Container 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

Configuration

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"
}
}

Finding Your Bulb IPs

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 controller

API Documentation

The FastAPI backend provides automatic OpenAPI documentation:

Core Endpoints

MethodEndpointDescription
GET/bulbsRetrieve all bulb states
GET/bulbs/{name}Get single bulb state
POST/bulbs/{name}/commandSend command to bulb (HSV, power, etc.)
POST/groups/commandBatch control multiple bulbs
POST/bulbs/syncForce refresh all bulb states from hardware
GET/groupsList available groups
WS/wsWebSocket for real-time state updates

Command Examples

# 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"

WebSocket Protocol

Connect to ws://localhost:8000/ws for real-time updates:

constws=newWebSocket('ws://localhost:8000/ws');ws.onmessage=(event)=>{constdata=JSON.parse(event.data);// Initial state on connectif(data.type==="initial_state"){console.log(`Received ${data.data.length} bulbs`);}// Real-time bulb updatesif(data.type==="bulb_update"){console.log(`Bulb ${data.data.name} changed`,data.data);}};

Architecture Deep Dive

Protocol Layer (backend/led_controller.py)

Direct TCP Socket Implementation - No external LED libraries

# Example: Power on commandcmd= [0x71, 0x23, 0x0F] # header, power_on, modecmd.append(sum(cmd) &0xFF) # checksumawaitwriter.write(bytes(cmd))
# RGB color commandcmd= [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)

State Management (backend/bulb_manager.py)

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 conditionsiftime_since_command<10:
return# Don't poll, use cached state# Exponential backoff for offline bulbsifconsecutive_failures>3:
poll_interval=600# 10 minutes

Color Space (backend/color_utils.py)

Custom HSV ↔ RGB implementation (not using colorsys):

defhsv_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 mappingsector=int(h/60) %6# ... mathematical transformation

Why custom implementation?

  • Standard library colorsys uses float ranges (0.0-1.0)
  • Direct integer RGB output (0-255) for LED commands
  • Optimized for real-time color wheel interactions

API Layer (backend/main.py)

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

Frontend Architecture

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

Development

Project Structure

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

Development Commands

# 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 Developmentcd 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 container

Environment Configuration

Development mode (.env.development):

VITE_API_BASE=http://192.168.2.2:8000VITE_WS_URL=ws://192.168.2.2:8000/ws

Production mode (.env.production):

VITE_API_BASE=/apiVITE_WS_URL=

Vite automatically selects the correct environment. In production, Nginx proxies /api/* and /ws to the FastAPI backend.

Testing the Protocol

Manual TCP test (without the full app):

importasyncioasyncdeftest_bulb():
reader, writer=awaitasyncio.open_connection('192.168.1.100', 5577)
# Power oncmd= [0x71, 0x23, 0x0F, 0xA3]
writer.write(bytes(cmd))
awaitwriter.drain()
# Set redcmd= [0x31, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x0F]
cmd.append(sum(cmd) &0xFF)
writer.write(bytes(cmd))
awaitwriter.drain()
writer.close()
asyncio.run(test_bulb())

Production Deployment

Current Deployment

  • 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)

Performance Metrics

  • 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)

Security Considerations

  • 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

⚠️ Not recommended for public internet exposure without adding authentication.

Skills Demonstrated

This project showcases:

  1. Network Protocol Analysis

    • Packet capture and reverse engineering
    • Binary protocol implementation
    • TCP socket programming with asyncio
  2. System Design

    • State management with race condition handling
    • WebSocket real-time communication
    • Request debouncing and optimization
  3. Mathematics & Algorithms

    • Color space transformations (HSV ↔ RGB)
    • Polar coordinate systems for UI
    • Checksum algorithms
  4. Full-Stack Development

    • Modern React with TypeScript
    • FastAPI REST + WebSocket APIs
    • Canvas-based interactive graphics
  5. DevOps & Deployment

    • Multi-stage Docker builds
    • Nginx reverse proxy configuration
    • Process management with Supervisor
    • Production monitoring and logging

License

MIT License - see LICENSE file for details.


Built from first principles when off-the-shelf solutions didn't exist.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

LED Controller

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.

LED Controller InterfacePython 3.11+ReactFastAPI

The Challenge: Reverse Engineering a Proprietary Protocol

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:

  1. Traffic Analysis: Captured TCP packets while using the vendor's mobile app
  2. Pattern Recognition: Identified command structures through hex dump analysis
  3. Protocol Decoding: Discovered command format: [header][r][g][b][warm_white][mode][checksum]
  4. Checksum Algorithm: Reverse-engineered simple sum-based checksum validation
  5. State Query Protocol: Decoded bidirectional status query/response mechanism

Result: Direct TCP control with <100ms latency, zero cloud dependencies, complete local network ownership.

Technical Highlights

Protocol Implementation

  • Async TCP Sockets - Native asyncio connection handling with 3-second timeouts
  • Binary Protocol - Direct byte-level command construction (0x31 RGB, 0x71 power)
  • Bidirectional Communication - Status queries return 14-byte response packets
  • Checksum Validation - Custom sum-based integrity checking

Color Space Mathematics

  • 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

System Architecture

  • 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

Quick Start

Prerequisites

  • 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

Installation

  1. Clone the repository

    git clone https://github.com/yourusername/led-controller.git
    cd led-controller
  2. Configure your bulbs

    cp config.example.json config.json
    # Edit config.json with your bulb IPs and names
  3. Install dependencies

    # Frontend
    bun install
    # Backend
    pip install fastapi uvicorn websockets
  4. Start the application

    # Terminal 1 - Backendcd backend && python main.py
    # Terminal 2 - Frontend
    bun run dev
  5. Open your browser

Docker Deployment (Recommended)

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 -d

Container 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

Configuration

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"
}
}

Finding Your Bulb IPs

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 controller

API Documentation

The FastAPI backend provides automatic OpenAPI documentation:

Core Endpoints

MethodEndpointDescription
GET/bulbsRetrieve all bulb states
GET/bulbs/{name}Get single bulb state
POST/bulbs/{name}/commandSend command to bulb (HSV, power, etc.)
POST/groups/commandBatch control multiple bulbs
POST/bulbs/syncForce refresh all bulb states from hardware
GET/groupsList available groups
WS/wsWebSocket for real-time state updates

Command Examples

# 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"

WebSocket Protocol

Connect to ws://localhost:8000/ws for real-time updates:

constws=newWebSocket('ws://localhost:8000/ws');ws.onmessage=(event)=>{constdata=JSON.parse(event.data);// Initial state on connectif(data.type==="initial_state"){console.log(`Received ${data.data.length} bulbs`);}// Real-time bulb updatesif(data.type==="bulb_update"){console.log(`Bulb ${data.data.name} changed`,data.data);}};

Architecture Deep Dive

Protocol Layer (backend/led_controller.py)

Direct TCP Socket Implementation - No external LED libraries

# Example: Power on commandcmd= [0x71, 0x23, 0x0F] # header, power_on, modecmd.append(sum(cmd) &0xFF) # checksumawaitwriter.write(bytes(cmd))
# RGB color commandcmd= [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)

State Management (backend/bulb_manager.py)

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 conditionsiftime_since_command<10:
return# Don't poll, use cached state# Exponential backoff for offline bulbsifconsecutive_failures>3:
poll_interval=600# 10 minutes

Color Space (backend/color_utils.py)

Custom HSV ↔ RGB implementation (not using colorsys):

defhsv_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 mappingsector=int(h/60) %6# ... mathematical transformation

Why custom implementation?

  • Standard library colorsys uses float ranges (0.0-1.0)
  • Direct integer RGB output (0-255) for LED commands
  • Optimized for real-time color wheel interactions

API Layer (backend/main.py)

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

Frontend Architecture

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

Development

Project Structure

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

Development Commands

# 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 Developmentcd 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 container

Environment Configuration

Development mode (.env.development):

VITE_API_BASE=http://192.168.2.2:8000VITE_WS_URL=ws://192.168.2.2:8000/ws

Production mode (.env.production):

VITE_API_BASE=/apiVITE_WS_URL=

Vite automatically selects the correct environment. In production, Nginx proxies /api/* and /ws to the FastAPI backend.

Testing the Protocol

Manual TCP test (without the full app):

importasyncioasyncdeftest_bulb():
reader, writer=awaitasyncio.open_connection('192.168.1.100', 5577)
# Power oncmd= [0x71, 0x23, 0x0F, 0xA3]
writer.write(bytes(cmd))
awaitwriter.drain()
# Set redcmd= [0x31, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x0F]
cmd.append(sum(cmd) &0xFF)
writer.write(bytes(cmd))
awaitwriter.drain()
writer.close()
asyncio.run(test_bulb())

Production Deployment

Current Deployment

  • 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)

Performance Metrics

  • 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)

Security Considerations

  • 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

⚠️ Not recommended for public internet exposure without adding authentication.

Skills Demonstrated

This project showcases:

  1. Network Protocol Analysis

    • Packet capture and reverse engineering
    • Binary protocol implementation
    • TCP socket programming with asyncio
  2. System Design

    • State management with race condition handling
    • WebSocket real-time communication
    • Request debouncing and optimization
  3. Mathematics & Algorithms

    • Color space transformations (HSV ↔ RGB)
    • Polar coordinate systems for UI
    • Checksum algorithms
  4. Full-Stack Development

    • Modern React with TypeScript
    • FastAPI REST + WebSocket APIs
    • Canvas-based interactive graphics
  5. DevOps & Deployment

    • Multi-stage Docker builds
    • Nginx reverse proxy configuration
    • Process management with Supervisor
    • Production monitoring and logging

License

MIT License - see LICENSE file for details.


Built from first principles when off-the-shelf solutions didn't exist.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

LED Controller

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.

LED Controller InterfacePython 3.11+ReactFastAPI

The Challenge: Reverse Engineering a Proprietary Protocol

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:

  1. Traffic Analysis: Captured TCP packets while using the vendor's mobile app
  2. Pattern Recognition: Identified command structures through hex dump analysis
  3. Protocol Decoding: Discovered command format: [header][r][g][b][warm_white][mode][checksum]
  4. Checksum Algorithm: Reverse-engineered simple sum-based checksum validation
  5. State Query Protocol: Decoded bidirectional status query/response mechanism

Result: Direct TCP control with <100ms latency, zero cloud dependencies, complete local network ownership.

Technical Highlights

Protocol Implementation

  • Async TCP Sockets - Native asyncio connection handling with 3-second timeouts
  • Binary Protocol - Direct byte-level command construction (0x31 RGB, 0x71 power)
  • Bidirectional Communication - Status queries return 14-byte response packets
  • Checksum Validation - Custom sum-based integrity checking

Color Space Mathematics

  • 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

System Architecture

  • 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

Quick Start

Prerequisites

  • 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

Installation

  1. Clone the repository

    git clone https://github.com/yourusername/led-controller.git
    cd led-controller
  2. Configure your bulbs

    cp config.example.json config.json
    # Edit config.json with your bulb IPs and names
  3. Install dependencies

    # Frontend
    bun install
    # Backend
    pip install fastapi uvicorn websockets
  4. Start the application

    # Terminal 1 - Backendcd backend && python main.py
    # Terminal 2 - Frontend
    bun run dev
  5. Open your browser

Docker Deployment (Recommended)

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 -d

Container 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

Configuration

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"
}
}

Finding Your Bulb IPs

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 controller

API Documentation

The FastAPI backend provides automatic OpenAPI documentation:

Core Endpoints

MethodEndpointDescription
GET/bulbsRetrieve all bulb states
GET/bulbs/{name}Get single bulb state
POST/bulbs/{name}/commandSend command to bulb (HSV, power, etc.)
POST/groups/commandBatch control multiple bulbs
POST/bulbs/syncForce refresh all bulb states from hardware
GET/groupsList available groups
WS/wsWebSocket for real-time state updates

Command Examples

# 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"

WebSocket Protocol

Connect to ws://localhost:8000/ws for real-time updates:

constws=newWebSocket('ws://localhost:8000/ws');ws.onmessage=(event)=>{constdata=JSON.parse(event.data);// Initial state on connectif(data.type==="initial_state"){console.log(`Received ${data.data.length} bulbs`);}// Real-time bulb updatesif(data.type==="bulb_update"){console.log(`Bulb ${data.data.name} changed`,data.data);}};

Architecture Deep Dive

Protocol Layer (backend/led_controller.py)

Direct TCP Socket Implementation - No external LED libraries

# Example: Power on commandcmd= [0x71, 0x23, 0x0F] # header, power_on, modecmd.append(sum(cmd) &0xFF) # checksumawaitwriter.write(bytes(cmd))
# RGB color commandcmd= [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)

State Management (backend/bulb_manager.py)

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 conditionsiftime_since_command<10:
return# Don't poll, use cached state# Exponential backoff for offline bulbsifconsecutive_failures>3:
poll_interval=600# 10 minutes

Color Space (backend/color_utils.py)

Custom HSV ↔ RGB implementation (not using colorsys):

defhsv_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 mappingsector=int(h/60) %6# ... mathematical transformation

Why custom implementation?

  • Standard library colorsys uses float ranges (0.0-1.0)
  • Direct integer RGB output (0-255) for LED commands
  • Optimized for real-time color wheel interactions

API Layer (backend/main.py)

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

Frontend Architecture

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

Development

Project Structure

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

Development Commands

# 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 Developmentcd 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 container

Environment Configuration

Development mode (.env.development):

VITE_API_BASE=http://192.168.2.2:8000VITE_WS_URL=ws://192.168.2.2:8000/ws

Production mode (.env.production):

VITE_API_BASE=/apiVITE_WS_URL=

Vite automatically selects the correct environment. In production, Nginx proxies /api/* and /ws to the FastAPI backend.

Testing the Protocol

Manual TCP test (without the full app):

importasyncioasyncdeftest_bulb():
reader, writer=awaitasyncio.open_connection('192.168.1.100', 5577)
# Power oncmd= [0x71, 0x23, 0x0F, 0xA3]
writer.write(bytes(cmd))
awaitwriter.drain()
# Set redcmd= [0x31, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x0F]
cmd.append(sum(cmd) &0xFF)
writer.write(bytes(cmd))
awaitwriter.drain()
writer.close()
asyncio.run(test_bulb())

Production Deployment

Current Deployment

  • 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)

Performance Metrics

  • 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)

Security Considerations

  • 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

⚠️ Not recommended for public internet exposure without adding authentication.

Skills Demonstrated

This project showcases:

  1. Network Protocol Analysis

    • Packet capture and reverse engineering
    • Binary protocol implementation
    • TCP socket programming with asyncio
  2. System Design

    • State management with race condition handling
    • WebSocket real-time communication
    • Request debouncing and optimization
  3. Mathematics & Algorithms

    • Color space transformations (HSV ↔ RGB)
    • Polar coordinate systems for UI
    • Checksum algorithms
  4. Full-Stack Development

    • Modern React with TypeScript
    • FastAPI REST + WebSocket APIs
    • Canvas-based interactive graphics
  5. DevOps & Deployment

    • Multi-stage Docker builds
    • Nginx reverse proxy configuration
    • Process management with Supervisor
    • Production monitoring and logging

License

MIT License - see LICENSE file for details.


Built from first principles when off-the-shelf solutions didn't exist.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

LED Controller

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.

LED Controller InterfacePython 3.11+ReactFastAPI

The Challenge: Reverse Engineering a Proprietary Protocol

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:

  1. Traffic Analysis: Captured TCP packets while using the vendor's mobile app
  2. Pattern Recognition: Identified command structures through hex dump analysis
  3. Protocol Decoding: Discovered command format: [header][r][g][b][warm_white][mode][checksum]
  4. Checksum Algorithm: Reverse-engineered simple sum-based checksum validation
  5. State Query Protocol: Decoded bidirectional status query/response mechanism

Result: Direct TCP control with <100ms latency, zero cloud dependencies, complete local network ownership.

Technical Highlights

Protocol Implementation

  • Async TCP Sockets - Native asyncio connection handling with 3-second timeouts
  • Binary Protocol - Direct byte-level command construction (0x31 RGB, 0x71 power)
  • Bidirectional Communication - Status queries return 14-byte response packets
  • Checksum Validation - Custom sum-based integrity checking

Color Space Mathematics

  • 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

System Architecture

  • 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

Quick Start

Prerequisites

  • 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

Installation

  1. Clone the repository

    git clone https://github.com/yourusername/led-controller.git
    cd led-controller
  2. Configure your bulbs

    cp config.example.json config.json
    # Edit config.json with your bulb IPs and names
  3. Install dependencies

    # Frontend
    bun install
    # Backend
    pip install fastapi uvicorn websockets
  4. Start the application

    # Terminal 1 - Backendcd backend && python main.py
    # Terminal 2 - Frontend
    bun run dev
  5. Open your browser

Docker Deployment (Recommended)

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 -d

Container 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

Configuration

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"
}
}

Finding Your Bulb IPs

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 controller

API Documentation

The FastAPI backend provides automatic OpenAPI documentation:

Core Endpoints

MethodEndpointDescription
GET/bulbsRetrieve all bulb states
GET/bulbs/{name}Get single bulb state
POST/bulbs/{name}/commandSend command to bulb (HSV, power, etc.)
POST/groups/commandBatch control multiple bulbs
POST/bulbs/syncForce refresh all bulb states from hardware
GET/groupsList available groups
WS/wsWebSocket for real-time state updates

Command Examples

# 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"

WebSocket Protocol

Connect to ws://localhost:8000/ws for real-time updates:

constws=newWebSocket('ws://localhost:8000/ws');ws.onmessage=(event)=>{constdata=JSON.parse(event.data);// Initial state on connectif(data.type==="initial_state"){console.log(`Received ${data.data.length} bulbs`);}// Real-time bulb updatesif(data.type==="bulb_update"){console.log(`Bulb ${data.data.name} changed`,data.data);}};

Architecture Deep Dive

Protocol Layer (backend/led_controller.py)

Direct TCP Socket Implementation - No external LED libraries

# Example: Power on commandcmd= [0x71, 0x23, 0x0F] # header, power_on, modecmd.append(sum(cmd) &0xFF) # checksumawaitwriter.write(bytes(cmd))
# RGB color commandcmd= [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)

State Management (backend/bulb_manager.py)

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 conditionsiftime_since_command<10:
return# Don't poll, use cached state# Exponential backoff for offline bulbsifconsecutive_failures>3:
poll_interval=600# 10 minutes

Color Space (backend/color_utils.py)

Custom HSV ↔ RGB implementation (not using colorsys):

defhsv_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 mappingsector=int(h/60) %6# ... mathematical transformation

Why custom implementation?

  • Standard library colorsys uses float ranges (0.0-1.0)
  • Direct integer RGB output (0-255) for LED commands
  • Optimized for real-time color wheel interactions

API Layer (backend/main.py)

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

Frontend Architecture

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

Development

Project Structure

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

Development Commands

# 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 Developmentcd 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 container

Environment Configuration

Development mode (.env.development):

VITE_API_BASE=http://192.168.2.2:8000VITE_WS_URL=ws://192.168.2.2:8000/ws

Production mode (.env.production):

VITE_API_BASE=/apiVITE_WS_URL=

Vite automatically selects the correct environment. In production, Nginx proxies /api/* and /ws to the FastAPI backend.

Testing the Protocol

Manual TCP test (without the full app):

importasyncioasyncdeftest_bulb():
reader, writer=awaitasyncio.open_connection('192.168.1.100', 5577)
# Power oncmd= [0x71, 0x23, 0x0F, 0xA3]
writer.write(bytes(cmd))
awaitwriter.drain()
# Set redcmd= [0x31, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x0F]
cmd.append(sum(cmd) &0xFF)
writer.write(bytes(cmd))
awaitwriter.drain()
writer.close()
asyncio.run(test_bulb())

Production Deployment

Current Deployment

  • 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)

Performance Metrics

  • 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)

Security Considerations

  • 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

⚠️ Not recommended for public internet exposure without adding authentication.

Skills Demonstrated

This project showcases:

  1. Network Protocol Analysis

    • Packet capture and reverse engineering
    • Binary protocol implementation
    • TCP socket programming with asyncio
  2. System Design

    • State management with race condition handling
    • WebSocket real-time communication
    • Request debouncing and optimization
  3. Mathematics & Algorithms

    • Color space transformations (HSV ↔ RGB)
    • Polar coordinate systems for UI
    • Checksum algorithms
  4. Full-Stack Development

    • Modern React with TypeScript
    • FastAPI REST + WebSocket APIs
    • Canvas-based interactive graphics
  5. DevOps & Deployment

    • Multi-stage Docker builds
    • Nginx reverse proxy configuration
    • Process management with Supervisor
    • Production monitoring and logging

License

MIT License - see LICENSE file for details.


Built from first principles when off-the-shelf solutions didn't exist.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

LED Controller

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.

LED Controller InterfacePython 3.11+ReactFastAPI

The Challenge: Reverse Engineering a Proprietary Protocol

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:

  1. Traffic Analysis: Captured TCP packets while using the vendor's mobile app
  2. Pattern Recognition: Identified command structures through hex dump analysis
  3. Protocol Decoding: Discovered command format: [header][r][g][b][warm_white][mode][checksum]
  4. Checksum Algorithm: Reverse-engineered simple sum-based checksum validation
  5. State Query Protocol: Decoded bidirectional status query/response mechanism

Result: Direct TCP control with <100ms latency, zero cloud dependencies, complete local network ownership.

Technical Highlights

Protocol Implementation

  • Async TCP Sockets - Native asyncio connection handling with 3-second timeouts
  • Binary Protocol - Direct byte-level command construction (0x31 RGB, 0x71 power)
  • Bidirectional Communication - Status queries return 14-byte response packets
  • Checksum Validation - Custom sum-based integrity checking

Color Space Mathematics

  • 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

System Architecture

  • 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

Quick Start

Prerequisites

  • 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

Installation

  1. Clone the repository

    git clone https://github.com/yourusername/led-controller.git
    cd led-controller
  2. Configure your bulbs

    cp config.example.json config.json
    # Edit config.json with your bulb IPs and names
  3. Install dependencies

    # Frontend
    bun install
    # Backend
    pip install fastapi uvicorn websockets
  4. Start the application

    # Terminal 1 - Backendcd backend && python main.py
    # Terminal 2 - Frontend
    bun run dev
  5. Open your browser

Docker Deployment (Recommended)

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 -d

Container 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

Configuration

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"
}
}

Finding Your Bulb IPs

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 controller

API Documentation

The FastAPI backend provides automatic OpenAPI documentation:

Core Endpoints

MethodEndpointDescription
GET/bulbsRetrieve all bulb states
GET/bulbs/{name}Get single bulb state
POST/bulbs/{name}/commandSend command to bulb (HSV, power, etc.)
POST/groups/commandBatch control multiple bulbs
POST/bulbs/syncForce refresh all bulb states from hardware
GET/groupsList available groups
WS/wsWebSocket for real-time state updates

Command Examples

# 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"

WebSocket Protocol

Connect to ws://localhost:8000/ws for real-time updates:

constws=newWebSocket('ws://localhost:8000/ws');ws.onmessage=(event)=>{constdata=JSON.parse(event.data);// Initial state on connectif(data.type==="initial_state"){console.log(`Received ${data.data.length} bulbs`);}// Real-time bulb updatesif(data.type==="bulb_update"){console.log(`Bulb ${data.data.name} changed`,data.data);}};

Architecture Deep Dive

Protocol Layer (backend/led_controller.py)

Direct TCP Socket Implementation - No external LED libraries

# Example: Power on commandcmd= [0x71, 0x23, 0x0F] # header, power_on, modecmd.append(sum(cmd) &0xFF) # checksumawaitwriter.write(bytes(cmd))
# RGB color commandcmd= [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)

State Management (backend/bulb_manager.py)

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 conditionsiftime_since_command<10:
return# Don't poll, use cached state# Exponential backoff for offline bulbsifconsecutive_failures>3:
poll_interval=600# 10 minutes

Color Space (backend/color_utils.py)

Custom HSV ↔ RGB implementation (not using colorsys):

defhsv_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 mappingsector=int(h/60) %6# ... mathematical transformation

Why custom implementation?

  • Standard library colorsys uses float ranges (0.0-1.0)
  • Direct integer RGB output (0-255) for LED commands
  • Optimized for real-time color wheel interactions

API Layer (backend/main.py)

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

Frontend Architecture

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

Development

Project Structure

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

Development Commands

# 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 Developmentcd 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 container

Environment Configuration

Development mode (.env.development):

VITE_API_BASE=http://192.168.2.2:8000VITE_WS_URL=ws://192.168.2.2:8000/ws

Production mode (.env.production):

VITE_API_BASE=/apiVITE_WS_URL=

Vite automatically selects the correct environment. In production, Nginx proxies /api/* and /ws to the FastAPI backend.

Testing the Protocol

Manual TCP test (without the full app):

importasyncioasyncdeftest_bulb():
reader, writer=awaitasyncio.open_connection('192.168.1.100', 5577)
# Power oncmd= [0x71, 0x23, 0x0F, 0xA3]
writer.write(bytes(cmd))
awaitwriter.drain()
# Set redcmd= [0x31, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x0F]
cmd.append(sum(cmd) &0xFF)
writer.write(bytes(cmd))
awaitwriter.drain()
writer.close()
asyncio.run(test_bulb())

Production Deployment

Current Deployment

  • 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)

Performance Metrics

  • 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)

Security Considerations

  • 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

⚠️ Not recommended for public internet exposure without adding authentication.

Skills Demonstrated

This project showcases:

  1. Network Protocol Analysis

    • Packet capture and reverse engineering
    • Binary protocol implementation
    • TCP socket programming with asyncio
  2. System Design

    • State management with race condition handling
    • WebSocket real-time communication
    • Request debouncing and optimization
  3. Mathematics & Algorithms

    • Color space transformations (HSV ↔ RGB)
    • Polar coordinate systems for UI
    • Checksum algorithms
  4. Full-Stack Development

    • Modern React with TypeScript
    • FastAPI REST + WebSocket APIs
    • Canvas-based interactive graphics
  5. DevOps & Deployment

    • Multi-stage Docker builds
    • Nginx reverse proxy configuration
    • Process management with Supervisor
    • Production monitoring and logging

License

MIT License - see LICENSE file for details.


Built from first principles when off-the-shelf solutions didn't exist.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

LED Controller

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.

LED Controller InterfacePython 3.11+ReactFastAPI

The Challenge: Reverse Engineering a Proprietary Protocol

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:

  1. Traffic Analysis: Captured TCP packets while using the vendor's mobile app
  2. Pattern Recognition: Identified command structures through hex dump analysis
  3. Protocol Decoding: Discovered command format: [header][r][g][b][warm_white][mode][checksum]
  4. Checksum Algorithm: Reverse-engineered simple sum-based checksum validation
  5. State Query Protocol: Decoded bidirectional status query/response mechanism

Result: Direct TCP control with <100ms latency, zero cloud dependencies, complete local network ownership.

Technical Highlights

Protocol Implementation

  • Async TCP Sockets - Native asyncio connection handling with 3-second timeouts
  • Binary Protocol - Direct byte-level command construction (0x31 RGB, 0x71 power)
  • Bidirectional Communication - Status queries return 14-byte response packets
  • Checksum Validation - Custom sum-based integrity checking

Color Space Mathematics

  • 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

System Architecture

  • 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

Quick Start

Prerequisites

  • 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

Installation

  1. Clone the repository

    git clone https://github.com/yourusername/led-controller.git
    cd led-controller
  2. Configure your bulbs

    cp config.example.json config.json
    # Edit config.json with your bulb IPs and names
  3. Install dependencies

    # Frontend
    bun install
    # Backend
    pip install fastapi uvicorn websockets
  4. Start the application

    # Terminal 1 - Backendcd backend && python main.py
    # Terminal 2 - Frontend
    bun run dev
  5. Open your browser

Docker Deployment (Recommended)

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 -d

Container 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

Configuration

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"
}
}

Finding Your Bulb IPs

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 controller

API Documentation

The FastAPI backend provides automatic OpenAPI documentation:

Core Endpoints

MethodEndpointDescription
GET/bulbsRetrieve all bulb states
GET/bulbs/{name}Get single bulb state
POST/bulbs/{name}/commandSend command to bulb (HSV, power, etc.)
POST/groups/commandBatch control multiple bulbs
POST/bulbs/syncForce refresh all bulb states from hardware
GET/groupsList available groups
WS/wsWebSocket for real-time state updates

Command Examples

# 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"

WebSocket Protocol

Connect to ws://localhost:8000/ws for real-time updates:

constws=newWebSocket('ws://localhost:8000/ws');ws.onmessage=(event)=>{constdata=JSON.parse(event.data);// Initial state on connectif(data.type==="initial_state"){console.log(`Received ${data.data.length} bulbs`);}// Real-time bulb updatesif(data.type==="bulb_update"){console.log(`Bulb ${data.data.name} changed`,data.data);}};

Architecture Deep Dive

Protocol Layer (backend/led_controller.py)

Direct TCP Socket Implementation - No external LED libraries

# Example: Power on commandcmd= [0x71, 0x23, 0x0F] # header, power_on, modecmd.append(sum(cmd) &0xFF) # checksumawaitwriter.write(bytes(cmd))
# RGB color commandcmd= [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)

State Management (backend/bulb_manager.py)

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 conditionsiftime_since_command<10:
return# Don't poll, use cached state# Exponential backoff for offline bulbsifconsecutive_failures>3:
poll_interval=600# 10 minutes

Color Space (backend/color_utils.py)

Custom HSV ↔ RGB implementation (not using colorsys):

defhsv_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 mappingsector=int(h/60) %6# ... mathematical transformation

Why custom implementation?

  • Standard library colorsys uses float ranges (0.0-1.0)
  • Direct integer RGB output (0-255) for LED commands
  • Optimized for real-time color wheel interactions

API Layer (backend/main.py)

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

Frontend Architecture

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

Development

Project Structure

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

Development Commands

# 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 Developmentcd 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 container

Environment Configuration

Development mode (.env.development):

VITE_API_BASE=http://192.168.2.2:8000VITE_WS_URL=ws://192.168.2.2:8000/ws

Production mode (.env.production):

VITE_API_BASE=/apiVITE_WS_URL=

Vite automatically selects the correct environment. In production, Nginx proxies /api/* and /ws to the FastAPI backend.

Testing the Protocol

Manual TCP test (without the full app):

importasyncioasyncdeftest_bulb():
reader, writer=awaitasyncio.open_connection('192.168.1.100', 5577)
# Power oncmd= [0x71, 0x23, 0x0F, 0xA3]
writer.write(bytes(cmd))
awaitwriter.drain()
# Set redcmd= [0x31, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x0F]
cmd.append(sum(cmd) &0xFF)
writer.write(bytes(cmd))
awaitwriter.drain()
writer.close()
asyncio.run(test_bulb())

Production Deployment

Current Deployment

  • 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)

Performance Metrics

  • 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)

Security Considerations

  • 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

⚠️ Not recommended for public internet exposure without adding authentication.

Skills Demonstrated

This project showcases:

  1. Network Protocol Analysis

    • Packet capture and reverse engineering
    • Binary protocol implementation
    • TCP socket programming with asyncio
  2. System Design

    • State management with race condition handling
    • WebSocket real-time communication
    • Request debouncing and optimization
  3. Mathematics & Algorithms

    • Color space transformations (HSV ↔ RGB)
    • Polar coordinate systems for UI
    • Checksum algorithms
  4. Full-Stack Development

    • Modern React with TypeScript
    • FastAPI REST + WebSocket APIs
    • Canvas-based interactive graphics
  5. DevOps & Deployment

    • Multi-stage Docker builds
    • Nginx reverse proxy configuration
    • Process management with Supervisor
    • Production monitoring and logging

License

MIT License - see LICENSE file for details.


Built from first principles when off-the-shelf solutions didn't exist.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

LED Controller

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.

LED Controller InterfacePython 3.11+ReactFastAPI

The Challenge: Reverse Engineering a Proprietary Protocol

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:

  1. Traffic Analysis: Captured TCP packets while using the vendor's mobile app
  2. Pattern Recognition: Identified command structures through hex dump analysis
  3. Protocol Decoding: Discovered command format: [header][r][g][b][warm_white][mode][checksum]
  4. Checksum Algorithm: Reverse-engineered simple sum-based checksum validation
  5. State Query Protocol: Decoded bidirectional status query/response mechanism

Result: Direct TCP control with <100ms latency, zero cloud dependencies, complete local network ownership.

Technical Highlights

Protocol Implementation

  • Async TCP Sockets - Native asyncio connection handling with 3-second timeouts
  • Binary Protocol - Direct byte-level command construction (0x31 RGB, 0x71 power)
  • Bidirectional Communication - Status queries return 14-byte response packets
  • Checksum Validation - Custom sum-based integrity checking

Color Space Mathematics

  • 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

System Architecture

  • 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

Quick Start

Prerequisites

  • 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

Installation

  1. Clone the repository

    git clone https://github.com/yourusername/led-controller.git
    cd led-controller
  2. Configure your bulbs

    cp config.example.json config.json
    # Edit config.json with your bulb IPs and names
  3. Install dependencies

    # Frontend
    bun install
    # Backend
    pip install fastapi uvicorn websockets
  4. Start the application

    # Terminal 1 - Backendcd backend && python main.py
    # Terminal 2 - Frontend
    bun run dev
  5. Open your browser

Docker Deployment (Recommended)

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 -d

Container 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

Configuration

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"
}
}

Finding Your Bulb IPs

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 controller

API Documentation

The FastAPI backend provides automatic OpenAPI documentation:

Core Endpoints

MethodEndpointDescription
GET/bulbsRetrieve all bulb states
GET/bulbs/{name}Get single bulb state
POST/bulbs/{name}/commandSend command to bulb (HSV, power, etc.)
POST/groups/commandBatch control multiple bulbs
POST/bulbs/syncForce refresh all bulb states from hardware
GET/groupsList available groups
WS/wsWebSocket for real-time state updates

Command Examples

# 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"

WebSocket Protocol

Connect to ws://localhost:8000/ws for real-time updates:

constws=newWebSocket('ws://localhost:8000/ws');ws.onmessage=(event)=>{constdata=JSON.parse(event.data);// Initial state on connectif(data.type==="initial_state"){console.log(`Received ${data.data.length} bulbs`);}// Real-time bulb updatesif(data.type==="bulb_update"){console.log(`Bulb ${data.data.name} changed`,data.data);}};

Architecture Deep Dive

Protocol Layer (backend/led_controller.py)

Direct TCP Socket Implementation - No external LED libraries

# Example: Power on commandcmd= [0x71, 0x23, 0x0F] # header, power_on, modecmd.append(sum(cmd) &0xFF) # checksumawaitwriter.write(bytes(cmd))
# RGB color commandcmd= [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)

State Management (backend/bulb_manager.py)

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 conditionsiftime_since_command<10:
return# Don't poll, use cached state# Exponential backoff for offline bulbsifconsecutive_failures>3:
poll_interval=600# 10 minutes

Color Space (backend/color_utils.py)

Custom HSV ↔ RGB implementation (not using colorsys):

defhsv_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 mappingsector=int(h/60) %6# ... mathematical transformation

Why custom implementation?

  • Standard library colorsys uses float ranges (0.0-1.0)
  • Direct integer RGB output (0-255) for LED commands
  • Optimized for real-time color wheel interactions

API Layer (backend/main.py)

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

Frontend Architecture

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

Development

Project Structure

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

Development Commands

# 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 Developmentcd 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 container

Environment Configuration

Development mode (.env.development):

VITE_API_BASE=http://192.168.2.2:8000VITE_WS_URL=ws://192.168.2.2:8000/ws

Production mode (.env.production):

VITE_API_BASE=/apiVITE_WS_URL=

Vite automatically selects the correct environment. In production, Nginx proxies /api/* and /ws to the FastAPI backend.

Testing the Protocol

Manual TCP test (without the full app):

importasyncioasyncdeftest_bulb():
reader, writer=awaitasyncio.open_connection('192.168.1.100', 5577)
# Power oncmd= [0x71, 0x23, 0x0F, 0xA3]
writer.write(bytes(cmd))
awaitwriter.drain()
# Set redcmd= [0x31, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x0F]
cmd.append(sum(cmd) &0xFF)
writer.write(bytes(cmd))
awaitwriter.drain()
writer.close()
asyncio.run(test_bulb())

Production Deployment

Current Deployment

  • 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)

Performance Metrics

  • 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)

Security Considerations

  • 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

⚠️ Not recommended for public internet exposure without adding authentication.

Skills Demonstrated

This project showcases:

  1. Network Protocol Analysis

    • Packet capture and reverse engineering
    • Binary protocol implementation
    • TCP socket programming with asyncio
  2. System Design

    • State management with race condition handling
    • WebSocket real-time communication
    • Request debouncing and optimization
  3. Mathematics & Algorithms

    • Color space transformations (HSV ↔ RGB)
    • Polar coordinate systems for UI
    • Checksum algorithms
  4. Full-Stack Development

    • Modern React with TypeScript
    • FastAPI REST + WebSocket APIs
    • Canvas-based interactive graphics
  5. DevOps & Deployment

    • Multi-stage Docker builds
    • Nginx reverse proxy configuration
    • Process management with Supervisor
    • Production monitoring and logging

License

MIT License - see LICENSE file for details.


Built from first principles when off-the-shelf solutions didn't exist.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

LED Controller

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.

LED Controller InterfacePython 3.11+ReactFastAPI

The Challenge: Reverse Engineering a Proprietary Protocol

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:

  1. Traffic Analysis: Captured TCP packets while using the vendor's mobile app
  2. Pattern Recognition: Identified command structures through hex dump analysis
  3. Protocol Decoding: Discovered command format: [header][r][g][b][warm_white][mode][checksum]
  4. Checksum Algorithm: Reverse-engineered simple sum-based checksum validation
  5. State Query Protocol: Decoded bidirectional status query/response mechanism

Result: Direct TCP control with <100ms latency, zero cloud dependencies, complete local network ownership.

Technical Highlights

Protocol Implementation

  • Async TCP Sockets - Native asyncio connection handling with 3-second timeouts
  • Binary Protocol - Direct byte-level command construction (0x31 RGB, 0x71 power)
  • Bidirectional Communication - Status queries return 14-byte response packets
  • Checksum Validation - Custom sum-based integrity checking

Color Space Mathematics

  • 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

System Architecture

  • 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

Quick Start

Prerequisites

  • 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

Installation

  1. Clone the repository

    git clone https://github.com/yourusername/led-controller.git
    cd led-controller
  2. Configure your bulbs

    cp config.example.json config.json
    # Edit config.json with your bulb IPs and names
  3. Install dependencies

    # Frontend
    bun install
    # Backend
    pip install fastapi uvicorn websockets
  4. Start the application

    # Terminal 1 - Backendcd backend && python main.py
    # Terminal 2 - Frontend
    bun run dev
  5. Open your browser

Docker Deployment (Recommended)

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 -d

Container 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

Configuration

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"
}
}

Finding Your Bulb IPs

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 controller

API Documentation

The FastAPI backend provides automatic OpenAPI documentation:

Core Endpoints

MethodEndpointDescription
GET/bulbsRetrieve all bulb states
GET/bulbs/{name}Get single bulb state
POST/bulbs/{name}/commandSend command to bulb (HSV, power, etc.)
POST/groups/commandBatch control multiple bulbs
POST/bulbs/syncForce refresh all bulb states from hardware
GET/groupsList available groups
WS/wsWebSocket for real-time state updates

Command Examples

# 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"

WebSocket Protocol

Connect to ws://localhost:8000/ws for real-time updates:

constws=newWebSocket('ws://localhost:8000/ws');ws.onmessage=(event)=>{constdata=JSON.parse(event.data);// Initial state on connectif(data.type==="initial_state"){console.log(`Received ${data.data.length} bulbs`);}// Real-time bulb updatesif(data.type==="bulb_update"){console.log(`Bulb ${data.data.name} changed`,data.data);}};

Architecture Deep Dive

Protocol Layer (backend/led_controller.py)

Direct TCP Socket Implementation - No external LED libraries

# Example: Power on commandcmd= [0x71, 0x23, 0x0F] # header, power_on, modecmd.append(sum(cmd) &0xFF) # checksumawaitwriter.write(bytes(cmd))
# RGB color commandcmd= [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)

State Management (backend/bulb_manager.py)

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 conditionsiftime_since_command<10:
return# Don't poll, use cached state# Exponential backoff for offline bulbsifconsecutive_failures>3:
poll_interval=600# 10 minutes

Color Space (backend/color_utils.py)

Custom HSV ↔ RGB implementation (not using colorsys):

defhsv_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 mappingsector=int(h/60) %6# ... mathematical transformation

Why custom implementation?

  • Standard library colorsys uses float ranges (0.0-1.0)
  • Direct integer RGB output (0-255) for LED commands
  • Optimized for real-time color wheel interactions

API Layer (backend/main.py)

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

Frontend Architecture

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

Development

Project Structure

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

Development Commands

# 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 Developmentcd 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 container

Environment Configuration

Development mode (.env.development):

VITE_API_BASE=http://192.168.2.2:8000VITE_WS_URL=ws://192.168.2.2:8000/ws

Production mode (.env.production):

VITE_API_BASE=/apiVITE_WS_URL=

Vite automatically selects the correct environment. In production, Nginx proxies /api/* and /ws to the FastAPI backend.

Testing the Protocol

Manual TCP test (without the full app):

importasyncioasyncdeftest_bulb():
reader, writer=awaitasyncio.open_connection('192.168.1.100', 5577)
# Power oncmd= [0x71, 0x23, 0x0F, 0xA3]
writer.write(bytes(cmd))
awaitwriter.drain()
# Set redcmd= [0x31, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x0F]
cmd.append(sum(cmd) &0xFF)
writer.write(bytes(cmd))
awaitwriter.drain()
writer.close()
asyncio.run(test_bulb())

Production Deployment

Current Deployment

  • 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)

Performance Metrics

  • 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)

Security Considerations

  • 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

⚠️ Not recommended for public internet exposure without adding authentication.

Skills Demonstrated

This project showcases:

  1. Network Protocol Analysis

    • Packet capture and reverse engineering
    • Binary protocol implementation
    • TCP socket programming with asyncio
  2. System Design

    • State management with race condition handling
    • WebSocket real-time communication
    • Request debouncing and optimization
  3. Mathematics & Algorithms

    • Color space transformations (HSV ↔ RGB)
    • Polar coordinate systems for UI
    • Checksum algorithms
  4. Full-Stack Development

    • Modern React with TypeScript
    • FastAPI REST + WebSocket APIs
    • Canvas-based interactive graphics
  5. DevOps & Deployment

    • Multi-stage Docker builds
    • Nginx reverse proxy configuration
    • Process management with Supervisor
    • Production monitoring and logging

License

MIT License - see LICENSE file for details.


Built from first principles when off-the-shelf solutions didn't exist.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages