A WiFi-enabled ESP32 LED pixel string controller with 24 stunning effects, 5 variations per effect, a physical button interface, and an embedded web dashboard for browser-based control. It also exposes a full REST API for programmatic control over HTTP and a built-in MCP (Model Context Protocol) server so AI assistants can drive the LEDs directly.
- 24 LED Effects - Fireflies, Rainbow Swipe, Aurora, Comet, Chasing Dots, Cylon, Dual Comet, Sparkle Sweep, Police, Plasma, Rainbow Gradient, Pulse Wave, Single Runner, Audio Visualizer, Heartbeat, Twinkle, Fire Flicker, Bouncing Balls, Lightning Storm, Kaleidoscope, Colliding Fill, Paint Splat, Snake, Glitter
- 5 Variations Per Effect - Cycle through variations with successive taps on the same button
- Web Dashboard - Embedded HTML/CSS/JS dashboard served directly from the ESP32, with animated icons and API feedback
- Saved Presets - Save and name your custom pixel configurations in the browser, then reapply them with one click
- Physical Button - Single-click to cycle effects, double-click for variations, long-press for power on/off
- Apple Home (HomeKit) - Exposes the strip as a HomeKit Lightbulb (on/off, brightness, color) via HomeSpan
- REST API - Full programmatic control over HTTP
- Static IP & mDNS - Reliable network configuration with custom hostname
- Persistent LED Count - Number of LEDs is stored in NVS (non-volatile storage) and configurable via API
- Auto-reconnect - Monitors WiFi and reconnects automatically
- ESP32 development board
- NeoPixel-compatible LED strip (WS2812B, SK6812, etc.)
- 3.3V-5V level shifter (recommended for longer strips)
- Physical button (optional, GPIO 9 with internal pull-up)
- 5V power supply (sized for your LED count)
| Component | GPIO Pin |
|---|---|
| LED Strip Data | GPIO 2 |
| Button (optional) | GPIO 9 (INPUT_PULLUP) |
git clone https://github.com/shajanjp/pixels-string.git
cd pixels-stringCopy the example config and fill in your network details:
cp config.example.h config.hEdit config.h:
#define WIFI_SSID "your-ssid"
#define WIFI_PASSWORD "your-password"
#define STATIC_IP IPAddress(192, 168, 1, 55)
#define GATEWAY IPAddress(192, 168, 1, 1)
#define SUBNET IPAddress(255, 255, 255, 0)
#define MDNS_HOSTNAME "pixels-string"
config.his gitignored - your credentials stay out of version control.
Edit site/dashboard.html, then regenerate the embedded header:
npm install
node html-to-header.jsThis minifies the HTML, CSS, and JS, and produces dashboard_html.h which is compiled into the firmware.
Open pixels-string.ino in the Arduino IDE or VS Code with PlatformIO, select your ESP32 board, and upload.
- Check your router's DHCP client list for the static IP you configured
- Or use mDNS:
http://pixels-string.local
The ESP32 serves a full-featured dashboard at its root URL. Each effect is shown as a card with an animated icon:
- Click a card to activate that effect
- Click the same card again to cycle through its 5 variations (a toast notification shows the variation number)
- The active effect is highlighted with a glowing border
The dashboard also has a Saved Presets panel. From the Custom Pixels modal you can give your current configuration a name and save it to the browser. It appears as a button on the home page (with a 7-dot preview painted to mirror your pattern) and can be reapplied with a single click. Presets store the pattern, colors, percentage, and custom ranges, and saving a name that already exists overwrites it. Each preset button has a small delete (×) control that appears on hover. Because presets are stored in the browser's localStorage (key pixelsString.presets), they are per-browser and do not require the device to save anything.
All endpoints are served on port 80.
GET /api/effect?name=<name> - Set the active effect by name.
Valid names:
FIREFLIES, RAINBOW_SWIPE, AURORA, COMET, CHASING_DOTS, CYLON, DUAL_COMET, SPARKLE_SWEEP, POLICE, PLASMA, RAINBOW_GRADIENT, PULSE_WAVE, SINGLE_RUNNER, AUDIO_VISUALIZER, HEARTBEAT, TWINKLE, FIRE_FLICKER, BOUNCING_BALLS, LIGHTNING_STORM, KALEIDOSCOPE, COLLIDING_FILL, PAINT_SPLAT, SNAKE, GLITTER
GET /api/effect?name=AURORA
GET /api/effect?index=<0-24> - Set effect by numeric index.
GET /api/effect?index=2 → Aurora
Note: index
15is the internalSTATIC_PIXELmode (used by/api/pixels/set). All other indexes 0-14 and 16-24 map to the public effects listed in the table below.
GET /api/variation?index=<0-4> - Set the variation of the current effect.
GET /api/variation?index=2
GET /api/power?state=<on|off> - Turn LEDs on or off.
GET /api/power?state=off
GET /api/brightness?value=<0-255> - Set the global LED brightness. Applied to every effect and pattern, and saved to NVS so it persists across reboots. 0 turns the LEDs off, 255 is maximum. Read the current value from /api/info.
GET /api/brightness?value=128
GET /api/info - Returns a JSON object with the current state.
{
"effect": "AURORA",
"effectIndex": 2,
"variation": 2,
"variationMax": 4,
"power": "on",
"numEffects": 25,
"ledCount": 50,
"brightness": 255,
"colorOrder": "GRB"
}GET /config - Returns the current device configuration as JSON: color order, LED count, brightness, active effect, and power state.
{
"colorOrder": "GRB",
"colorOrderValue": 2,
"ledCount": 50,
"brightness": 255,
"effect": "AURORA",
"power": "on"
}GET /config?colorOrder=<RGB|RBG|GRB|GBR|BRG|BGR> - Set the NeoPixel color order dynamically. This matches the physical wiring of the strip: some strips are RGB, others are GRB, BRG, BGR, etc. Accepts the color order by name (case-insensitive) or by a numeric index (0=RGB, 1=RBG, 2=GRB, 3=GBR, 4=BRG, 5=BGR, e.g. 5 = BGR). Applies immediately to all effects and patterns, is saved to NVS, and persists across reboots.
GET /config?colorOrder=BGR
GET /config?ledCount=<1-300> - Set the number of LEDs. Applies immediately, is saved to NVS, and persists across reboots.
GET /config?ledCount=100
Both parameters can be combined in a single request, e.g. GET /config?colorOrder=GRB&ledCount=120. Any request to /config (with or without parameters) returns the full current configuration as JSON, so the response serves as confirmation:
{
"colorOrder": "GRB",
"colorOrderValue": 2,
"ledCount": 120,
"brightness": 255,
"effect": "AURORA",
"power": "on"
}GET /help - Plain-text listing of all available endpoints.
GET /api/pixels/set - Set pixel colors with customizable patterns. All parameters are passed as query strings.
Query Parameters
| Parameter | Required | Description |
|---|---|---|
pattern |
Yes | Lighting pattern: solid, striped, gradient, or custom. |
color |
Yes* | Hex color code(s) without #. Can be repeated for multiple colors. Not used by custom. |
range |
Yes* | (Custom only) START-END:COLOR, 1-indexed and inclusive. Can be repeated up to 20 times. |
brightness |
No | Sets the global brightness 0-255. Persisted and applied to all effects (including this pattern). Omit to keep the current global brightness. |
percentage |
No | Applies the pattern to only the first N% of LEDs (0-100); the remaining LEDs are turned off. Omit or use 100 for the whole strip. Ignored by custom. |
*color is required for solid/striped/gradient; range is required for custom.
Color Format
- 6-digit hex:
RRGGBB(e.g.FF0000for red). - Optionally allows 3-digit shorthand:
F00->FF0000. - Case insensitive (
ff0000==FF0000). - Multiple colors are supplied by repeating the
colorkey.
Custom Pattern (ranges)
Each range param paints a solid color over a contiguous block of LEDs, using 1-indexed inclusive bounds (START and END are both LED numbers, 1 to the LED count). Up to 20 ranges are accepted. Where ranges overlap, the later range wins. LEDs not covered by any range keep their previous value.
Examples
GET /api/pixels/set?pattern=solid&color=FF0000
GET /api/pixels/set?pattern=striped&color=FF0000&color=00FF00&color=0000FF
GET /api/pixels/set?pattern=gradient&color=FF0000&color=0000FF&brightness=128
GET /api/pixels/set?pattern=gradient&color=FF0000&color=0000FF&percentage=50
GET /api/pixels/set?pattern=custom&range=1-10:FF0000&range=11-20:0000FF&range=21-25:00FF00
The device speaks the Model Context Protocol over HTTP (POST /mcp, JSON-RPC 2.0), so AI assistants (Claude Desktop, MCP Inspector, etc.) can control the LEDs directly. Register it as a remote MCP server with the device URL:
{
"mcpServers": {
"pixels-string": {
"url": "http://192.168.1.55/mcp"
}
}
}Two tools are exposed, both backed by the existing effect/pattern/brightness code:
Switch the strip to a built-in animation by name (case-insensitive, same names as /api/effect).
| Argument | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Animation name, e.g. FIREFLIES, AURORA, SNAKE |
Light the strip in a custom solid color at the given brightness (reuses the solid-pattern + global brightness pipeline).
| Argument | Type | Required | Description |
|---|---|---|---|
color |
string | Yes | Hex color, 3 or 6 digits, case-insensitive (FF0000 or f00) |
brightness |
integer | No | 0-255; keeps the current brightness when omitted. Persisted to NVS like /api/brightness. |
No session or SSE stream is required: each request is answered with a JSON-RPC response and the connection closes, so the endpoint works with any MCP client that supports HTTP servers.
Thanks to the HomeSpan library, the ESP32 also exposes the LED strip to Apple Home as a Lightbulb accessory. Once paired you can control it from the Home app, Control Center, and with Siri.
The device exposes two accessories:
| Accessory | Apple Home control | Device action |
|---|---|---|
| LED Strip (Lightbulb) | On / Off | Power on/off |
| Brightness slider | Global brightness (0-100%) | |
| Color wheel | Solid color (switches to static color mode) | |
| LED Effects (Switches) | Each effect is a Switch | Tap one to run that animation |
Turning on any effect switch runs that animation (e.g. the "AURORA" switch runs the aurora effect) and powers the strip on. Only one effect switch stays ON at a time. The LED Strip lightbulb represents solid-color mode: using it (on/off/brightness/color) puts the strip back into solid color.
Because HomeKit has no native "effect/mode" menu, each of the 24 animations is exposed as its own switch. You can trim this list in
homekit.hif you only want a few favorites (delete the correspondingnew EffectSwitch(...)lines).
HomeSpan takes over WiFi connection and mDNS (it must own the connection to start its HomeKit server). It keeps your static IP from config.h, and its HAP server runs on port 8080 so it does not clash with the REST API on port 80.
HomeSpan makes the firmware ~1.58 MB, which is too large for the default partition scheme (1.3 MB app slot). If you upload without changing it you will get:
Sketch too big
Compilation error: text section exceeds available space in board
To fix it, change the Partition Scheme to give the app a 2 MB slot:
- In the Arduino IDE: Tools > Partition Scheme > No OTA (2MB APP/2MB SPIFFS)
- With
arduino-cli:
arduino-cli compile --fqbn "esp32:esp32:esp32:PartitionScheme=no_ota" .This is required. It also changes the flash layout, so the first upload after switching will re-partition the chip.
- Flash the sketch and power the ESP32.
- Open the Serial Monitor (115200 baud) and watch HomeSpan start up. It prints the device's pairing QR and setup code.
- On your iPhone/iPad, open the Home app >
+> Add Accessory, and scan the QR (or tap More options and enter the setup code). - The device appears as LED Strip.
The setup code is set in
homekit.h(HOMESPAN_SETUP_CODE, default46637726). Change it to any 8 digits and re-flash, or change it later from the HomeSpan CLI (typeSin the serial monitor).
To un-pair or reset pairing data, use the HomeSpan CLI (U in the serial monitor) or hold the HomeSpan control action.
| # | Effect | Variations |
|---|---|---|
| 0 | FIREFLIES | 1 / 2 / 3 / 5 / 8 fireflies (speed scales with count) |
| 1 | RAINBOW_SWIPE | Forward / Reverse / Fast / Slow / Medium+Forward |
| 2 | AURORA | Blue-green / Warm / Cool violet / Pink-green / Rainbow-tinted |
| 3 | COMET | Position-hue / Fixed hue / Time-hue / White / Rainbow tail |
| 4 | CHASING_DOTS | 2 / 3 / 4 / 5 / 6 chasers |
| 5 | CYLON | Hue shift at ends / Hue shift + / Time-hue / White / Red |
| 6 | DUAL_COMET | White flash / Rainbow flash / Color-ring flash / Yellow / Red flash |
| 7 | SPARKLE_SWEEP | Density and width combos (5 variations) |
| 8 | POLICE | Alternating / Split halves / Three-segment / All flash / Sweeping |
| 9 | PLASMA | 2 / 3 / 4 / 5 / 5 blobs (wider) |
| 10 | RAINBOW_GRADIENT | Window size 6 / 10 / 14 / 18 / 22 |
| 11 | PULSE_WAVE | Sigma & speed combinations (5 variations) |
| 12 | SINGLE_RUNNER | Random hue / Rainbow-position / Time-hue / White / Tail trail |
| 13 | AUDIO_VISUALIZER | Simulated spectrum analyzers with varying band counts & styles |
| 14 | HEARTBEAT | Mac breathing / Lub-dub center / Alternating side / Travelling / Dual pulse |
| 16 | TWINKLE | White slow / Golden / Multicolor / Dense fast / Cold blue + supernovae |
| 17 | FIRE_FLICKER | Candle / Campfire / Inferno / Blue flame / Rainbow fire |
| 18 | BOUNCING_BALLS | 1 slow ball / 2 balls / 3 sized balls / 4 white with trails / 5 rainbow elastic |
| 19 | LIGHTNING_STORM | Full-strip flash / Random segments / Rolling wave / Violet afterglow / Frequent storm |
| 20 | KALEIDOSCOPE | 2-fold mirror / 4-fold twinkle / 8-fold plasma / Rotating washes / Laser-show centre |
| 21 | COLLIDING_FILL | Classic ends-inward / Colour swap / Teleport fill / Random side / Center-out fill |
| 22 | PAINT_SPLAT | Classic mix / Splash burst / Lava lamp / Rainbow bands / Galactic stars |
| 23 | SNAKE | Classic green / Speed boost / Rainbow / Obstacle course / Poison food |
| 24 | GLITTER | Transition speeds, slow→rapid (5 variations); 30-60% of LEDs lit, each easing between palette colours |
Index 15 is the internal
STATIC_PIXELmode used by/api/pixels/set; it has a single variation and is intentionally omitted from the table above and from the button cycle. The public effects therefore occupy indexes 0-14 and 16-24.
| Action | Result |
|---|---|
| Single click | Cycle to the next effect |
| Double click | Cycle variation (0 → 1 → 2 → 3 → 4 → 0) |
| Long press (2s) | Toggle power on/off |
pixels-string/
├── pixels-string.ino # Main sketch: globals, utilities, button, WiFi, setup/loop
├── globals.h # Shared declarations: Effect enum, externs, prototypes
├── effects.h # LED animations, static pixel patterns, effect lifecycle
├── homekit.h # Apple HomeKit (HomeSpan) Lightbulb accessory
├── mcp.h # Minimal MCP server (JSON-RPC 2.0 at POST /mcp)
├── rest_api.h # REST endpoints for the web dashboard (/api/*, /config, /help)
├── config.example.h # WiFi & network config template
├── config.h # Your actual config (gitignored)
├── dashboard_html.h # Auto-generated: minified dashboard embedded as a C string
├── html-to-header.js # Node.js script to minify site/dashboard.html into dashboard_html.h
├── package.json # Node dependencies (html-minifier)
├── test-mcp.sh # Curl-based test harness for the MCP endpoint
├── .mcp.json # MCP server registration (for MCP clients)
├── .github/
│ └── workflows/static.yml # Deploys the site/ folder to GitHub Pages
└── site/ # Web dashboard source + project website
├── dashboard.html # Web dashboard source (HTML/CSS/JS) → dashboard_html.h
├── index.html # Project landing page
├── favicon.png # Browser icon
├── apple-touch-icon.png # iOS home screen icon
├── og-image.png # Social sharing image
├── site.webmanifest # Web app manifest
├── pixels-string-dashboard-screenshot.jpg
└── pixels-string-dashboard-mobile-screenshot.jpg
All sketch files are compiled as a single translation unit: pixels-string.ino
includes the module headers and defines the shared globals they reference via
globals.h. The dashboard source lives in site/dashboard.html and is minified
into dashboard_html.h by html-to-header.js before compilation.
The software limit is 300 LEDs (MAX_LEDS). Adjust this in globals.h if your
hardware supports fewer or more (within ESP32 memory constraints).
ISC