WiFi · ESP32 · NeoPixel

Pixels String

A WiFi-enabled ESP32 LED pixel string controller with 24 stunning effects, 5 variations per effect, a physical button interface, an embedded web dashboard for browser-based control, native MCP support for AI assistants, and Apple Home (HomeKit) integration so you can control it from the Home app and Siri.

Get Started View REST API
ESPESP32 Powered APIREST API MCPAI Assistant Ready DASHBuilt-in Dashboard HKApple Home NVSPersistence WiFiWiFi + mDNS

# Overview

Pixels String turns an ESP32 dev board and a NeoPixel-compatible LED strip into a self-contained ambient lighting system. It runs fully on-device, with no cloud services or phone apps required.

The device hosts its own web dashboard directly from the ESP32, so any browser on your network can control every effect, variation, color, and brightness. A physical button gives you the same control without a browser. A complete REST API lets you integrate the strip into scripts, home automation, or other projects.

All settings are persisted in NVS (non-volatile storage), so your LED count, color order, brightness, and active effect survive reboots and power loss. WiFi is monitored continuously and reconnects automatically.

Dashboard Preview

What's in the box

Features & Capabilities

Everything the controller does out of the box.

24

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, and Glitter.

5x

5 Variations Per Effect

Every effect ships with 5 distinct variations. Tap the same button (or click the same card) to cycle through them, from firefly counts to color palettes to speeds.

WWW

Web Dashboard

A full HTML/CSS/JS dashboard is served straight from the ESP32. Animated icons, glowing active-effect highlight, toast notifications, and instant API feedback.

BTN

Physical Button

Single-click cycles effects, double-click cycles variations, and a 2-second long-press toggles power. Debounced with internal pull-up on GPIO 9.

API

REST API

9 HTTP endpoints for effects, variations, power, brightness, pixel patterns, and configuration. JSON responses make scripting and automation trivial.

MCP

MCP for AI Assistants

Speaks the Model Context Protocol over HTTP (POST /mcp, JSON-RPC 2.0), so Claude Desktop, MCP Inspector, and other assistants can run animations and set colors directly.

HK

Apple Home (HomeKit)

Built on the HomeSpan library, the strip appears in Apple Home as a Lightbulb with on/off, brightness, and color wheel control - plus a Switch for each of the 24 effects. Control it from the Home app or with Siri.

RGB

Custom Range Control

Paint exact blocks of LEDs with the Custom Pixels modal - dual-slider ranges, a live strip preview, and up to 20 color segments via the custom pattern.

SAVE

Saved Presets

Name and save your current custom configuration in the browser, then reapply it with a single click from a dedicated presets panel on the dashboard - complete with a color-dot preview.

IP

Static IP & mDNS

Configurable static IP with gateway and subnet, plus an mDNS hostname (default pixels-string.local) so you never have to hunt for the device.

NVS

Persistent Settings

LED count, color order, and brightness are stored in NVS and survive reboots. The active effect and power state persist in memory during a session.

WIFI

Auto-Reconnect

WiFi is monitored continuously and reconnects automatically with a 10-second retry loop, keeping the dashboard and API reachable.

# Effects & Variations

All 24 effects and what each variation does. Set any effect with /api/effect?name=... or by index.

#EffectVariations
0FIREFLIES1 / 2 / 3 / 5 / 8 fireflies (speed scales with count)
1RAINBOW_SWIPEForward / Reverse / Fast / Slow / Medium+Forward
2AURORABlue-green / Warm / Cool violet / Pink-green / Rainbow-tinted
3COMETPosition-hue / Fixed hue / Time-hue / White / Rainbow tail
4CHASING_DOTS2 / 3 / 4 / 5 / 6 chasers
5CYLONHue shift at ends / Hue shift+ / Time-hue / White / Red
6DUAL_COMETWhite flash / Rainbow flash / Color-ring flash / Yellow / Red flash
7SPARKLE_SWEEPDensity and width combos (5 variations)
8POLICEAlternating / Split halves / Three-segment / All flash / Sweeping
9PLASMA2 / 3 / 4 / 5 / 5 blobs (wider)
10RAINBOW_GRADIENTWindow size 6 / 10 / 14 / 18 / 22
11PULSE_WAVESigma & speed combinations (5 variations)
12SINGLE_RUNNERRandom hue / Rainbow-position / Time-hue / White / Tail trail
13AUDIO_VISUALIZERSimulated spectrum analyzers with varying band counts & styles
14HEARTBEATMac breathing / Lub-dub center / Alternating side / Travelling / Dual pulse
16TWINKLEWhite slow / Golden / Multicolor / Dense fast / Cold blue + supernovae
17FIRE_FLICKERCandle / Campfire / Inferno / Blue flame / Rainbow fire
18BOUNCING_BALLS1 slow ball / 2 balls / 3 sized balls / 4 white with trails / 5 rainbow elastic
19LIGHTNING_STORMFull-strip flash / Random segments / Rolling wave / Violet afterglow / Frequent storm
20KALEIDOSCOPE2-fold mirror / 4-fold twinkle / 8-fold plasma / Rotating washes / Laser-show centre
21COLLIDING_FILLClassic ends-inward / Colour swap / Teleport fill / Random side / Center-out fill
22PAINT_SPLATClassic mix / Splash burst / Lava lamp / Rainbow bands / Galactic stars
23SNAKEClassic green / Speed boost / Rainbow / Obstacle course / Poison food
24GLITTERTransition speeds, slow→rapid (5 variations); 30-60% of LEDs lit, each easing between palette colours
Variations are indexed 0-4. Index 15 is the internal STATIC_PIXEL mode used by /api/pixels/set; it has a single variation and is intentionally omitted here (and from the button cycle). The public effects occupy indexes 0-14 and 16-24.

# Quick Start

From a fresh clone to a glowing strip - requirements, wiring, setup, and configuration.

# Hardware Requirements

You need an ESP32, an LED strip, and a power supply. Everything else is optional.

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

Pin Connections

ComponentGPIO PinNotes
LED Strip DataGPIO 2Default PIN in the sketch
Button (optional)GPIO 9Wired as INPUT_PULLUP to GND
The software limit is MAX_LEDS = 300 in globals.h. Adjust it if your hardware supports fewer or more, within ESP32 memory constraints.

# Getting Started

From a fresh clone to a glowing strip in five steps.

1. Clone and configure

git clone https://github.com/shajanjp/pixels-string.git
cd pixels-string

2. WiFi and network config

Copy the example config and fill in your network details:

cp config.example.h config.h

Then edit 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.h is gitignored, so your credentials never end up in version control.

3. (Optional) Modify the dashboard

The dashboard is embedded in the firmware. Edit dashboard.html, then regenerate the embedded header:

npm install
node html-to-header.js

This minifies the HTML, CSS, and JS and produces dashboard_html.h, which is compiled into the firmware.

4. Upload to your ESP32

Open pixels-string.ino in the Arduino IDE or VS Code with PlatformIO, select your ESP32 board, and upload. Required libraries:

  • Adafruit_NeoPixel - LED strip driver
  • HomeSpan - Apple HomeKit support (Lightbulb + effect switches)
  • WiFi, ESPmDNS, Preferences - included with the ESP32 core
HomeSpan makes the firmware ~1.58 MB, too large for the default partition scheme (1.3 MB app slot). If you get Sketch too big, switch the partition scheme to No OTA (2MB APP/2MB SPIFFS) under Tools > Partition Scheme. This is required, and the first upload after switching will re-partition the chip.

5. Find your device

  • Check your router's DHCP client list for the static IP you configured
  • Or use mDNS: http://pixels-string.local

# Configuration

What lives in config.h and what can be changed at runtime.

Network settings (config.h)

SettingDefaultDescription
WIFI_SSID"your-ssid"WiFi network name
WIFI_PASSWORD"your-password"WiFi password
STATIC_IP192.168.1.55Static IP assigned to the device
GATEWAY192.168.1.1Network gateway
SUBNET255.255.255.0Network subnet mask
MDNS_HOSTNAME"pixels-string"mDNS name, resolves to pixels-string.local

Runtime settings (persisted in NVS)

These are configured via the API and survive reboots:

SettingRangeDescription
ledCount1 - 300Number of LEDs on the strip
colorOrderRGB / RBG / GRB / GBR / BRG / BGRNeoPixel color order matching your strip's wiring
brightness0 - 255Global brightness applied to every effect
Why color order matters: some strips wire channels as RGB, others as GRB, BRG, BGR, and so on. If colors look swapped, set the correct order via /config?colorOrder=... - it applies immediately and is saved to NVS.

# Control & API

The web dashboard, the physical button, the REST API, and MCP for AI assistants.

# Using the Device

Control the strip two ways: the web dashboard or the physical button.

Web dashboard

Open the device's root URL (http://pixels-string.local or your static IP) in any browser. The ESP32 serves a full-featured dashboard where every effect is 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 exposes power and brightness controls plus a Custom Pixels editor with solid, striped, gradient, and custom range patterns, all with live API feedback.

Saved Presets

A Saved Presets panel lets you keep and reuse your custom looks. From the Custom Pixels modal, give your current configuration a name and save it - the preset appears as a button on the dashboard's home page, painted with a 7-dot preview that mirrors your pattern:

  • Save - name and save the current pattern, colors, percentage, and custom ranges from the Custom Pixels modal
  • Reapply with one click - clicking a preset button sends the same /api/pixels/set request as the modal's Apply button
  • Overwrite - saving a name that already exists replaces that preset
  • Delete - a small × control appears on each preset button on hover
  • Per-browser - presets are stored in the browser's localStorage, so no state is saved on the device

Physical button controls

ActionResult
Single clickCycle to the next effect
Double clickCycle variation (0 -> 1 -> 2 -> 3 -> 4 -> 0)
Long press (2s)Toggle power on/off

# REST API

All endpoints are served on port 80 and accept plain GET requests with query parameters. JSON endpoints return readable, unescaped JSON for easy parsing.

Tip: run GET /help on the device for a plain-text listing of every available endpoint.
GET /api/effect?name=<name>

Set the active effect by name or by numeric index.

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

GET /api/effect?name=AURORA
GET /api/effect?index=2    # index 2 = Aurora
Index 15 is the internal STATIC_PIXEL mode (used by /api/pixels/set). All other indexes 0-14 and 16-24 map to the public effects above.
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 the 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 device state.

{
  "effect": "AURORA",
  "effectIndex": 2,
  "variation": 2,
  "variationMax": 4,
  "power": "on",
  "numEffects": 24,
  "ledCount": 50,
  "brightness": 255,
  "colorOrder": "GRB"
}
GET /config?colorOrder=<order>&ledCount=<n>

Read or set the device configuration. Without parameters it returns the current config as JSON. Parameters apply immediately, are saved to NVS, and persist across reboots. Either or both parameters can be combined in one request.

colorOrder optional RGB, RBG, GRB, GBR, BRG, or BGR (case-insensitive), or a numeric index 0-5 (e.g. 5 = BGR; 2 = GRB)
ledCount optional Number of LEDs, 1-300
GET /config                              # read current config
GET /config?colorOrder=BGR               # set color order
GET /config?ledCount=100                 # set LED count
GET /config?colorOrder=GRB&ledCount=120  # both at once

Every request to /config returns the full configuration as JSON, so the response doubles as confirmation:

{
  "colorOrder": "GRB",
  "colorOrderValue": 2,
  "ledCount": 120,
  "brightness": 255,
  "effect": "AURORA",
  "power": "on"
}
GET /api/pixels/set?pattern=<p>[&color=RRGGBB... | &range=START-END:COLOR...][&brightness=0-255][&percentage=0-100]

Set pixel colors with customizable patterns. All parameters are passed as query strings.

pattern required Lighting pattern: solid, striped, gradient, or custom.
color required* Hex color code(s) without #. Can be repeated for multiple colors. Required for solid/striped/gradient; not used by custom.
range required* START-END:COLOR, 1-indexed and inclusive. Can be repeated up to 20 times. Required for custom only.
brightness optional Sets global brightness 0-255, persisted and applied to all effects. Omit to keep the current global brightness.
percentage optional 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 format

  • 6-digit hex: RRGGBB (e.g. FF0000 for red)
  • 3-digit shorthand allowed: F00 = FF0000
  • Case insensitive (ff0000 == FF0000)
  • Multiple colors supplied by repeating the color key

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
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
GET /help

Plain-text listing of all available endpoints. Handy when working on a headless device.

# MCP (Model Context Protocol)

The device speaks the Model Context Protocol over HTTP, so AI assistants can control the LEDs directly.

The ESP32 hosts a minimal MCP server at POST /mcp (JSON-RPC 2.0). Register it as a remote MCP server in any MCP client - Claude Desktop, MCP Inspector, and others - using the device URL:

{
  "mcpServers": {
    "pixels-string": {
      "url": "http://192.168.1.55/mcp"
    }
  }
}

Exposed tools

Two tools are exposed, both backed by the existing effect, pattern, and brightness code:

ToolWhat it does
apply_animationSwitch the strip to a built-in animation by name (case-insensitive, same names as /api/effect)
set_colorLight the strip in a custom solid color at a given brightness (reuses the solid-pattern + global brightness pipeline)

apply_animation

ArgumentTypeRequiredDescription
namestringYesAnimation name, e.g. FIREFLIES, AURORA, SNAKE

set_color

ArgumentTypeRequiredDescription
colorstringYesHex color, 3 or 6 digits, case-insensitive (FF0000 or f00)
brightnessintegerNo0-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.

# Apple Home (HomeKit)

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.

What HomeKit controls

The device exposes two accessories.

AccessoryApple Home controlDevice action
LED Strip (Lightbulb)On / OffPower on/off
Brightness sliderGlobal brightness (0-100%)
Color wheelSolid color (switches to static color mode)
LED Effects (Switches)Each effect is a SwitchTap 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.h if you only want a few favorites (delete the corresponding new EffectSwitch(...) lines).

Wiring & networking

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.

Required partition scheme (IMPORTANT - do this first)

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 / text section exceeds available space in board. To fix it, 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.

Pairing with Apple Home

  1. Flash the sketch and power the ESP32.
  2. Open the Serial Monitor (115200 baud) and watch HomeSpan start up. It prints the device’s pairing QR and setup code.
  3. On your iPhone/iPad, open the Home app > + > Add Accessory, and scan the QR (or tap More options and enter the setup code).
  4. The device appears as LED Strip.
The setup code is set in homekit.h (HOMESPAN_SETUP_CODE, default 46637726). Change it to any 8 digits and re-flash, or change it later from the HomeSpan CLI (type S in 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.

# Project Structure

A quick map of the repository.

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 + effect switches
├── 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         # Web dashboard source (HTML/CSS/JS)
├── dashboard_html.h       # Auto-generated: minified HTML embedded as a C string
├── html-to-header.js      # Node.js script to minify HTML & generate header
├── package.json           # Node dependencies (html-minifier)
├── pixels-string-dashboard-screenshot.jpg
└── site/                  # Project website
    ├── index.html         # 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
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.