Open-source firmware for ESP8266/ESP32-based RGBWW(CW) LED controllers, supporting up to 5 independent PWM channels (R, G, B, WW, CW). Built on the Sming framework.
This is a fork of Patrick Jahns' original firmware and VBS's extension, significantly extended with multi-controller synchronisation, a rich JSON HTTP API, MQTT with Home Assistant auto-discovery, OTA updates, mDNS, rsyslog support, and a modern Vue/Quasar web application.
- Supported Hardware
- Features
- Installation & Flashing
- OTA Updates
- Initial Setup
- Configuration Reference
- HTTP API Reference
- Color Command Reference
- MQTT Integration
- Multi-Controller Synchronisation
- Remote Logging (rsyslog)
- Event Server
- WebSocket API
- Security
- Building from Source
- Integration: FHEM
- Contributing
| SoC | Status | PWM frequency |
|---|---|---|
| ESP8266 | ✅ Primary | 800 Hz |
| ESP32 | ✅ Supported | 4 000 Hz |
| ESP32-C3 | ✅ Supported | configurable |
| Name | SoC | R | G | B | WW | CW | Clear |
|---|---|---|---|---|---|---|---|
| mrpj | ESP8266 | 13 | 12 | 14 | 5 | 4 | 16 |
| Lightinator Mini | ESP32-C3 | 0 | 4 | 5 | 6 | 7 | 18 |
| NodeMCU ESP32-C3 01M | ESP32-C3 | 5 | 7 | 6 | 9 | 10 | 21 |
| Shojo PCB (WemosD1 mini) | ESP8266 | 14 | 4 | 5 | 15 | 12 | 16 |
Pin configurations are fetched at runtime from a remote JSON file (default:
https://raw.githubusercontent.com/pljakobs/esp_rgb_webapp2/devel/public/config/pinconfig.json)
and can be overridden in the configuration.
- Up to 5 independent PWM output channels (RGB, RGBWW, RGBCW, RGBWWCW)
- Smooth, programmable on-board fades and animations
- Independent per-channel animation queues with configurable queue policies
- Relative color commands (+/- values on any channel)
- Command requeuing enabling seamless animation loops
- Pause / continue / stop / skip animation queue controls
- Instant blink command
- Toggle (on ↔ off)
- Ramp speed and ramp time specification
- Startup color:
LAST(restore previous) orSET(fixed color) - Hardware push-button support with configurable debounce
- HTTP JSON API on port 80
- WebSocket push events (color changes, transitions, notifications)
- MQTT publish/subscribe with optional Home Assistant auto-discovery
- TCP event server for state-change broadcasts
- mDNS/Bonjour device advertisement
- NTP time synchronization
- UDP rsyslog logging to any syslog collector
- OTA firmware updates from configurable update URL
- Dual-ROM partition layout — safe A/B rollback on ESP8266/ESP32
- LittleFS for persistent config storage
- Captive portal for initial Wi-Fi setup
- Basic HTTP authentication for API access
- HSV color model (hue 0–359°, saturation 0–100, value 0–100)
- HSVCT: HSV extended with colour temperature
- RAW 8-bit channel output (r, g, b, ww, cw)
- Per-channel brightness scaling (0–100%)
- Warm-white / cold-white Kelvin calibration (default 2700 K / 6000 K)
- Gamma correction
- 12-segment parabolic HSV calibration (per 30° hue sector)
- Parabolic colour-temperature curve calibration
- Multiple output modes (RGB, RGBWW, RGBCW, RGBWWCW)
Pre-compiled binaries are published as GitHub Actions artifacts on every successful build.
esptool.py --port /dev/ttyUSB0 write_flash 0x00000 firmware.binRefer to the Wiki — Flashing for platform-specific instructions and address maps.
0x000000 Partition table
0x002000 rom0 (active firmware)
0x100000 lfs0 (~1 000 kB, active LittleFS)
0x202000 rom1 (OTA target)
0x300000 lfs1 (~1 000 kB, fallback LittleFS)
Firmware updates are managed through the webapp's Firmware Update dialog (Settings → Firmware Update → Check firmware). The dialog fetches a firmware manifest from the configured update server and presents three cascading selectors:
- Branch —
Stable,Testing, orDevelop(colour-coded: green/amber/red) - Build type —
releaseordebug - Version — specific build, with date shown in the dropdown
The dialog shows the currently running firmware version, branch, and webapp version for comparison. Selecting a firmware entry and clicking Update triggers the OTA process with live progress reporting via WebSocket.
The update server URL defaults to https://lightinator.de/version.json and is configurable via the /config API key ota.url.
Important: An active OTA process blocks
/config,/color, and other write endpoints; the API returns{"error":"update in progress"}(HTTP 400) during this time.
The manifest format served at the update URL is a JSON array of firmware entries, each containing branch, type, fw_version, date, and a direct download URL for the binary.
Note: Command-line / scripted OTA is not yet documented. A TUI update tool or integration into the Lightinator Log Service companion is planned.
On every boot the firmware runs the following sequence:
Boot
└─ No SSID stored?
├─ Yes → first-run mode
│ ├─ Start AP: SSID = "Lightinator_<chipid>", IP = 192.168.4.1
│ ├─ Start DNS captive portal (redirects all DNS → 192.168.4.1)
│ └─ Scan for available networks (results cached for /networks)
│
└─ No → station mode
├─ Static IP configured? → apply IP/mask/gateway, disable DHCP
└─ DHCP (default) → enable DHCP
Connect → retry up to DEFAULT_CONNECTION_RETRIES times
├─ Connected
│ ├─ Set hostname (device_name, or "Lightinator_<chipid>")
│ ├─ Start network services (HTTP, WebSocket, event server)
│ ├─ Get IP → start mDNS, register controller, start MQTT (if enabled)
│ ├─ Report any saved crash dump via syslog
│ └─ Stop AP after short delay (1 s normal / 90 s new-connection)
│
└─ Failed / wrong password
├─ Start AP (fallback)
└─ Scan for networks
- Power on — with no credentials stored the controller starts an access point:
- SSID:
Lightinator_<chipid>(e.g.Lightinator_12AB34) - Password:
rgbwwctrl - IP:
192.168.4.1
- SSID:
- Connect a phone or laptop to the AP network.
- Open the web UI at
http://192.168.4.1/— a DNS captive portal redirects all traffic there automatically. - Enter Wi-Fi credentials (SSID / password) in Settings → Network and save. The controller scans for available networks on boot, so they are already listed in the dropdown.
- The controller connects to your network. The AP stays up for 90 seconds to allow the UI to follow the redirect before shutting down.
- Once connected the device is reachable at:
- IP address shown in the station-connected notification (or via your router's DHCP table)
<device_name>.localvia mDNS (once a device name is configured)
- Set the pin configuration for your hardware in Settings → Hardware (
general.current_pin_config_name). A reboot is required for pin changes to take effect.
If the controller cannot reach the saved network (wrong password, out of range, etc.) after the configured number of retries, it:
- Falls back to its own AP so you can always reach it to reconfigure
- Continues scanning for the target network in the background
- Reports the error via the event server / WebSocket (
connection_statusnotification)
Configuration is read and written via GET /config and POST /config using a flat JSON document.
Changes take effect immediately for most settings; settings noted below require a reboot.
| Key | Type | Default | Description |
|---|---|---|---|
device_name |
string | "" |
Hostname used for mDNS and MQTT client ID |
current_pin_config_name |
string | "mrpj" |
Active hardware pin map name (reboot required) |
pin_config_url |
string | remote URL | URL for the pin configuration manifest |
supported_color_models |
array | ["RGB","RGBWW","RGBCW","RGBWWCW"] |
Models shown in UI |
buttons_debounce_ms |
integer | 50 | Hardware button debounce time |
allow_web_icons |
boolean | true | Allow the webapp to load remote icons |
| Key | Type | Default | Description |
|---|---|---|---|
color_mode |
integer | 0 | Output mode: 0=RGB, 1=RGBWW, 2=RGBCW, 3=RGBWWCW (reboot required) |
startup.mode |
string | "LAST" |
Startup colour: LAST or SET |
startup.color |
string | — | Colour string when startup.mode is SET |
brightness.red/green/blue/ww/cw |
integer | 100 | Per-channel brightness correction (0–100) |
colortemp.ww |
integer | 2700 | Warm-white colour temperature in Kelvin |
colortemp.cw |
integer | 6000 | Cold-white colour temperature in Kelvin |
calibration.gamma |
number | — | Output gamma (e.g. 2.2) |
calibration.hsv |
array[12] | — | Parabolic coefficients per 30° hue sector |
calibration.ct |
object | — | Parabolic coefficients for CT curve |
hsv.red/green/blue/… |
integer | 0 | HSV hue shift per colour sector |
| Key | Type | Default | Description |
|---|---|---|---|
enabled |
boolean | false | Enable MQTT client |
server |
string | "mqtt.local" |
Broker hostname or IP |
port |
integer | 1883 | Broker TCP port |
username |
string | "" |
Broker username |
password |
string | "" |
Broker password |
topic_base |
string | "home/" |
Base topic prefix |
homeassistant.enable |
boolean | true | Publish HA discovery messages |
homeassistant.discovery_prefix |
string | "homeassistant" |
HA discovery prefix |
homeassistant.node_id |
string | "" |
HA node ID (defaults to device name) |
| Key | Type | Default | Description |
|---|---|---|---|
dhcp |
boolean | true | Use DHCP (disable for static IP, reboot required) |
ip |
string | "192.168.1.1" |
Static IP (reboot required) |
netmask |
string | "255.255.255.0" |
Static netmask |
gateway |
string | "192.168.1.255" |
Static gateway |
| Key | Type | Default | Description |
|---|---|---|---|
ssid |
string | "Lightinator_<chipid>" |
Access-point SSID |
password |
string | "configesp" |
Access-point password |
secured |
boolean | true | Require AP password |
| Key | Type | Default | Description |
|---|---|---|---|
enabled |
boolean | false | Send logs via UDP syslog |
host |
string | — | Collector hostname or IP |
port |
integer | 514 | Collector UDP port |
| Key | Type | Default | Description |
|---|---|---|---|
server_enabled |
boolean | true | Enable TCP event server |
color_interval_ms |
integer | 500 | Minimum ms between colour events |
color_min_interval_ms |
integer | 500 | Hard minimum interval |
trans_fin_interval_ms |
integer | 1000 | Interval for transition-finished events |
See Multi-Controller Synchronisation.
All endpoints are on port 80. Responses are JSON. When security is enabled, HTTP Basic Auth
(Authorization: Basic <base64>) is required on every request.
Global response codes
| HTTP | Body | Meaning |
|---|---|---|
| 200 | {"success":true} |
Command accepted |
| 400 | {"error":"<msg>"} |
Bad request / missing param |
| 401 | — | Unauthorised (security enabled) |
| 429 | — | Insufficient heap; retry after 4 s |
| 503 | — | OTA in progress (ESP8266 only) |
CORS headers are always present. OPTIONS requests are answered with 200 {"success":true}.
Returns device identity and runtime state.
Query parameter: ?v=2 — returns the extended v2 format (recommended).
v2 response:
{
"device": {
"deviceid": 3221339823,
"soc": "esp8266",
"current_rom": "rom0"
},
"app": {
"git_version": "abc1234",
"build_type": "release",
"git_date": "2026-04-05",
"webapp_version": "1.2.3"
},
"sming": { "version": "5.1.0" },
"runtime": {
"uptime": 3600,
"heap_free": 24576,
"minimumfreeHeapRuntime": 20000,
"minimumfreeHeap10min": 21000,
"heapLowErrUptime": 0,
"heapLowErr10min": 0
},
"rgbww": { "version": "2.0.0", "queuesize": 10 },
"connection": {
"connected": true,
"ssid": "MyNetwork",
"dhcp": true,
"ip": "192.168.1.50",
"netmask": "255.255.255.0",
"gateway": "192.168.1.1",
"mac": "AA:BB:CC:DD:EE:FF"
},
"mqtt": {
"enabled": true,
"status": "running",
"broker": "mqtt.local",
"topic": "home/"
}
}Returns the current output state.
{
"raw": { "r": 255, "g": 128, "b": 0, "ww": 0, "cw": 0 },
"hsv": { "h": 1.047, "s": 1.0, "v": 1.0, "ct": 0 }
}Note: HSV values in the GET response are in radians (h: 0–2π, s/v: 0–1). POST commands use degrees (0–359) and 0–100 percent.
Send a color command. See Color Command Reference.
Returns the full configuration as JSON. Suitable for backup and programmatic inspection.
Writes a partial or full configuration update. Only provided keys are changed.
curl -X POST http://<device>/config \
-H "Content-Type: application/json" \
-d '{"network":{"rsyslog":{"enabled":true,"host":"192.168.1.10","port":5514}}}'Side effects on write:
| Setting changed | Effect |
|---|---|
| Static IP | Delayed reboot (3 s) |
| SSID | Delayed reboot (3 s) |
| DHCP → static | Delayed reboot |
| DHCP → DHCP | Applied immediately |
| MQTT enabled/disabled | Started/stopped immediately |
| Color mode | Delayed reboot (1 s) |
| Pin config name | Delayed reboot (1 s) |
| Device name | Applied immediately, mDNS updated |
| rsyslog host/port | Applied immediately |
| rsyslog enabled | Applied immediately |
| Telemetry | Started/stopped immediately |
Returns available Wi-Fi networks (up to 25). Trigger a scan first with POST /scan_networks.
{
"scanning": false,
"available": [
{ "id": 12345, "ssid": "MyNetwork", "signal": -62, "encryption": "WPA2" }
]
}Initiates an asynchronous Wi-Fi scan. Poll GET /networks for results.
Submit Wi-Fi credentials to connect to a network.
{ "ssid": "MyNetwork", "password": "s3cr3t" }Simple availability check. Returns {"success":true}.
System control commands.
cmd |
Description |
|---|---|
restart |
Reboot the device |
factory |
Reset to factory defaults |
debug |
Enable/disable debug output (also requires "enable": true/false) |
curl -X POST http://<device>/system -d '{"cmd":"restart"}'Turn the output on (restore last colour) or off immediately.
Toggle between on and off.
Stop animation and clear queue. Equivalent to skip + clear.
Skip the current animation step, jumping immediately to its end state.
Pause the animation queue.
Resume a paused animation queue.
Trigger an instant blink of the current color.
{ "ramp": 500 }Read and write the application data store (groups, scenes, presets, controller registry).
Returns known peer controllers registered in the data store.
Triggers a firmware OTA check and update from the configured update URL.
The /color endpoint (POST) and the MQTT color topic accept the same JSON command format.
{
"cmd": "fade",
"hsv": { "h": 120, "s": 100, "v": 80 },
"ramp": 2000,
"direction": 0,
"queue": "single",
"requeue": false,
"name": "my-fade"
}{
"cmd": "solid",
"raw": { "r": 255, "g": 0, "b": 0, "ww": 128, "cw": 0 },
"ramp": 0
}{
"cmds": [
{ "cmd": "fade", "hsv": { "h": 0, "s": 100, "v": 100 }, "ramp": 1000 },
{ "cmd": "fade", "hsv": { "h": 120, "s": 100, "v": 100 }, "ramp": 1000, "requeue": true }
]
}Animate from an explicit start colour to a target:
{
"cmd": "fade",
"hsv": { "h": 240, "s": 100, "v": 80 },
"hsvFrom": { "h": 0, "s": 100, "v": 80 },
"ramp": 3000
}Any numeric field can be specified as a relative string (delta):
{ "cmd": "solid", "hsv": { "h": "+30", "s": "0", "v": "-10" } }Address specific PWM channels (0=R, 1=G, 2=B, 3=WW, 4=CW) without affecting others:
{ "cmd": "fade", "hsv": { "v": 50 }, "channels": [3, 4], "ramp": 1000 }| Field | Type | Description |
|---|---|---|
cmd |
string | "solid" (default) or "fade" |
hsv |
object | HSV target: h (0–359), s (0–100), v (0–100), ct (0–10000) |
raw |
object | RAW target: r, g, b, ww, cw (0–255) |
hsvFrom |
object | Explicit HSV start (optional) |
rawFrom |
object | Explicit RAW start (optional) |
ramp |
number | Transition time in ms, or speed value |
direction |
integer | Hue rotation direction: 0=forward, 1=reverse |
queue |
string | Queue policy: "single", "back", "front", "replace" |
requeue |
boolean | Re-insert command at queue end after completion (looping) |
name |
string | Command name (reported in transition-finished events) |
channels |
array | Target specific channel indices only |
MQTT is disabled by default. Enable via the configuration.
| Topic | Payload | Retain | Description |
|---|---|---|---|
<base>color |
{"raw":{...},"cmd":"solid","t":0} |
yes | Current output (raw) |
<base>color |
{"hsv":{...},"cmd":"solid","t":0} |
yes | Current output (HSV) |
last/will |
"The connection from this device is lost :(" |
yes | LWT |
Where <base> is the configured topic_base (default: home/).
| Config key | Purpose |
|---|---|
sync.color_slave_topic |
Receive color commands |
sync.cmd_slave_topic |
Receive JSON-RPC commands |
sync.clock_slave_topic |
Receive master clock ticks |
When network.mqtt.homeassistant.enable is true, the firmware publishes MQTT discovery messages for:
- A main
lightentity (full RGBWW control) - Individual channel
lightentities
Discovery prefix defaults to homeassistant. Node ID defaults to the device name.
Multiple controllers can be synchronised to play identical animations in lock-step. One controller acts as master, the others as slaves.
The master publishes a tick counter at sync.clock_master_interval ms intervals. Slaves receive ticks and dynamically adjust their PWM timer interval using a PLL-style algorithm to eliminate accumulated drift.
When sync.cmd_master_enabled is true, every color/animation command received by the master is re-published to the command slave topic. Slaves with sync.cmd_slave_enabled execute these commands, achieving synchronised playback.
An alternative mode (sync.color_master_enabled) publishes the master's current output values at sync.color_master_interval_ms for less-precise but simpler following.
| Key | Default | Description |
|---|---|---|
sync.clock_master_enabled |
false | Publish master clock ticks |
sync.clock_master_interval |
30 ms | Clock publish interval |
sync.clock_slave_enabled |
false | Follow master clock |
sync.clock_slave_topic |
"home/led1/clock" |
Clock MQTT topic |
sync.cmd_master_enabled |
false | Relay commands to slaves |
sync.cmd_slave_enabled |
false | Execute relayed commands |
sync.cmd_slave_topic |
"home/led/command" |
Command relay MQTT topic |
sync.color_master_enabled |
false | Publish colour state |
sync.color_master_interval_ms |
0 | Colour publish interval |
sync.color_slave_enabled |
false | Follow master colour |
sync.color_slave_topic |
"home/led1/command" |
Colour MQTT topic |
The firmware can forward log messages to any UDP syslog collector (RFC 3164 / RFC 5424) in real time.
Enable via /config:
{
"network": {
"rsyslog": {
"enabled": true,
"host": "192.168.1.10",
"port": 5514
}
}
}Changes take effect immediately without a reboot.
A companion container service (lightinator-log-service) provides:
- UDP syslog ingestion (port 5514 by default)
- HTTP API for log retrieval and live tail
- Integration with the webapp Log Viewer
Setup flow:
- Run the collector container on a local host.
- In the webapp Log Viewer, enter the collector's IPv4 and HTTP port.
- The webapp configures the controller's rsyslog target automatically.
- Logs appear live in the web interface.
When events.server_enabled is true, the controller broadcasts state-change notifications over a persistent TCP connection. Compatible with FHEM and custom integrations.
Event types pushed:
color— current colour value (at most everycolor_interval_ms)transition_finished— emitted after an animation command completes (includesnameandrequeuedflag)notification— human-readable messages (config changes, reboots, etc.)
Connect to ws://<device>/ws. The controller pushes the same events as the TCP event server:
{ "type": "color", "data": { "raw": { "r": 255 }, "hsv": { "h": 1.047 } } }
{ "type": "transition_finished", "data": { "name": "my-fade", "requeued": false } }
{ "type": "notification", "data": "new SSID, MyNetwork" }The webapp uses this connection for live status updates.
Disabled by default. Enable via:
{ "security": { "api_secured": true, "api_password": "mysecret" } }When enabled, every HTTP request must include Authorization: Basic <base64(:<password>)>. There is no username — use just the password preceded by a colon in the base64-encoded string.
The AP password defaults to configesp and should be changed before deployment.
- Sming framework installed and
SMING_HOMEset make,esptool, and the appropriate GCC toolchain for your target SoC
# ESP8266
make SMING_ARCH=Esp8266
# ESP32
make SMING_ARCH=Esp32
# Host (unit tests / simulation)
make SMING_ARCH=Hostmake flash SMING_ARCH=Esp8266 COM_PORT=/dev/ttyUSB0The flash and deployOta scripts in the repository also handle OTA deployment to running devices.
A ready-made FHEM device module is available: https://github.com/verybadsoldier/esp_rgbww_fhemmodule
See also the FHEM Forum thread for community support.
Pull requests are welcome. Please read CONTRIBUTING.md before submitting.