WLED is C++ firmware for ESP32/ESP8266 microcontrollers controlling addressable LEDs, with a web UI (HTML/JS/CSS). Built with PlatformIO (Arduino framework) and Node.js tooling.
See also: .github/copilot-instructions.md, .github/agent-build.instructions.md,
docs/cpp.instructions.md, docs/web.instructions.md, docs/cicd.instructions.md,
docs/hardening.instructions.md, docs/securecode.instructions.md.
Always reference these instructions - including coding guidelines in docs/ - first and fallback to search or bash commands only when you encounter unexpected information that does not match the info here.
| Command | Purpose | Timeout |
|---|---|---|
npm ci |
Install Node.js deps (required first) | 30s |
npm run build |
Build web UI into wled00/html_*.h / wled00/js_*.h |
30s |
npm test |
Run test suite (Node.js built-in node --test) |
2 min |
npm run dev |
Watch mode — auto-rebuilds web UI on changes | continuous |
pio run -e esp32dev |
Build firmware (ESP32, most common target) | 5 min |
pio run -e nodemcuv2 |
Build firmware (ESP8266) | 5 min |
Always run npm ci && npm run build before pio run. The web UI build generates
required C headers for firmware compilation.
Tests use Node.js built-in test runner (node:test). The single test file is
tools/cdata-test.js. Run it with:
npm test # runs all tests via `node --test`
node --test tools/cdata-test.js # run just that file directlyThere are no C++ unit tests. Firmware is validated by successful compilation across
target environments. Always build after code changes: pio run -e esp32dev.
esp32dev, nodemcuv2, esp8266_2m, esp32c3dev, esp32s3dev_8MB_opi, lolin_s2_mini
npm run build -- -f # force web UI rebuild
rm -f wled00/html_*.h wled00/js_*.h && npm run build # clean + rebuild UI
pio run --target clean # clean PlatformIO build artifacts
rm -rf node_modules && npm ci # reinstall Node.js depswled00/ # Main firmware source (C++)
data/ # Web UI source (HTML/JS/CSS) — tabs for indentation
html_*.h, js_*.h # Auto-generated (NEVER edit or commit)
src/ # Sub-modules: fonts, bundled dependencies (ArduinoJSON)
usermods/ # Community usermods (each has library.json + .cpp/.h)
platformio.ini # Build configuration and environments
pio-scripts/ # PlatformIO build scripts (Python)
tools/ # Node.js build tools (cdata.js) and tests
docs/ # Coding convention docs
.github/workflows/ # CI/CD (GitHub Actions)
main # Main development trunk (daily/nightly) 17.0.0-dev. Target branch for PRs.
├── V5 # special branch: code rework for esp-idf 5.5.x (unstable)
├── V5-C6 # special branch: integration of new MCU types: esp32-c5, esp32-c6, esp32-p4 (unstable)
16_x # maintenance for release 16.0.x
0_15_x # maintenance (bugfixes only) for previous release 0.15.x
(tag) v0.14.4 # old version 0.14.4 (no maintenance)
(tag) v0.13.3 # old version 0.13.3 (no maintenance)
(tag) v0. ... . ... # historical versions 0.12.x and before
- 2-space indentation (no tabs in C++ files)
- K&R brace style preferred (opening brace on same line)
- Single-statement
ifbodies may omit braces:if (a == b) doStuff(a); - Space after keywords (
if (...),for (...)), no space before function parens (doStuff(a)) - No enforced line-length limit
//for inline (always space after),/* */for block comments- Important: AI-generated source code blocks must be mark with
// AI: below section was generated by an AI/// AI: end
| Kind | Convention | Examples |
|---|---|---|
| Functions, variables | camelCase | setRandomColor(), effectCurrent |
| Classes, structs | PascalCase | BusConfig, UsermodTemperature |
| Macros, constants | UPPER_CASE | WLED_MAX_USERMODS, FX_MODE_STATIC |
| Private members | _camelCase | _type, _bri, _len |
| Enum values | PascalCase | PinOwner::BusDigital |
- Include
"wled.h"as the primary project header - Project headers first, then platform/Arduino, then third-party
- Platform-conditional includes wrapped in
#ifdef ARDUINO_ARCH_ESP32/#ifdef ESP8266
- Prefer
const &for read-only function parameters - Mark getter/query methods
const; usestaticfor methods not accessing instance state - Prefer
constexprover#definefor compile-time constants when possible - Use
static_assertover#if ... #error - Use
uint_fast16_t/uint_fast8_tin hot-path code
- No C++ exceptions — some builds disable them
- Use return codes (
false,-1) and global flags (errorFlag = ERR_LOW_MEM) - Use early returns as guard clauses:
if (!enabled || (strip.isUpdating() && (millis() - last_time < MAX_USERMOD_DELAY))) return; - Debug output:
DEBUG_PRINTF()/DEBUG_PRINTLN()(compiled out unless-D WLED_DEBUG)
- Use
F("string")for string constants (saves RAM on ESP8266) - Use
PSTR()withDEBUG_PRINTF_P()for format strings - Avoid
Stringin hot paths; acceptable in config/setup code - Use
d_malloc()(DRAM-preferred) /p_malloc()(PSRAM-preferred) for allocation - No VLAs — use fixed arrays or heap allocation
- Call
reserve()on strings/vectors to pre-allocate and avoid fragmentation
- Check availability: Test chip availability with
psramFound() && ESP.getPsramSize() > 0before assuming PSRAM is present. Never rely onBOARD_HAS_PSRAMonly. - DMA compatibility: on ESP32 (classic), PSRAM buffers are not DMA-capable. On ESP32-S3 with octal PSRAM (
CONFIG_SPIRAM_MODE_OCT), PSRAM buffers can be used with DMA whenCONFIG_SOC_PSRAM_DMA_CAPABLEis defined. - Fragmentation: PSRAM allocations fragment less than DRAM because the region is larger. But avoid mixing small and large allocations in PSRAM — small allocations waste the MMU page granularity.
- Performance: Prefer DRAM (or IRAM) for hot-path data that is frequently used. Prefer PSRAM for capacity-oriented buffers where slightly slower access times can be tolerated.
Background Info:
- PSRAM access is up to 18× slower than DRAM on ESP32 (dual-SPI bus), 3–10× slower than DRAM on ESP32-S3/-S2 with quad-SPI bus. On ESP32-S3 with octal PSRAM (
CONFIG_SPIRAM_MODE_OCT), the penalty is smaller (~2×) because the 8-line DTR bus can transfer 8 bits in parallel. On ESP32-P4 with hex PSRAM (CONFIG_SPIRAM_MODE_HEX), the 16-line bus runs at 200 MHz which brings it on-par with DRAM. - Consider that ESP32 often crashes when the largest DRAM chunk gets below 10 KB.
- Feature toggling:
WLED_DISABLE_*andWLED_ENABLE_*flags (exact names matter!) WLED_DISABLE_*:2D,ADALIGHT,ALEXA,MQTT,OTA,INFRARED,WEBSOCKETS, etc.WLED_ENABLE_*:DMX,GIF,HUB75MATRIX,JSONLIVE,WEBSOCKETS, etc.- Platform:
ARDUINO_ARCH_ESP32,ESP8266,CONFIG_IDF_TARGET_ESP32S3
- Use
sin8_t(),cos8_t()— NOTsin8(),cos8()(removed, won't compile) - Use
sin_approx()/cos_approx()instead ofsinf()/cosf() - Replace
inoise8/inoise16withperlin8/perlin16
- Use function attributes:
IRAM_ATTR,WLED_O2_ATTR,__attribute__((hot)) - Cache class members to locals before loops
- Pre-compute invariants outside loops; use reciprocals to avoid division
- Unsigned range checks:
if ((uint_fast16_t)(pix - start) < len)
delay(1)in custom FreeRTOS tasks (NOTyield()) — feeds IDLE watchdog- Do not use
delay()in effects (FX.cpp) or hot pixel path
- Use FreeRTOS mutexes, semaphores or queues when true concurrent access from multiple FreeRTOS tasks is possible, and race-conditions can lead to unexpected behaviour.
- Avoid
portENTER_CRITICAL()/portEXIT_CRITICAL(), as these functions stall the complete system and may cause LEDs flickering. Prefer FreeRTOS mutexes, semaphores or queues. - Important: Not every shared resource needs a mutex. Some synchronization is guaranteed by the overall control flow, for example when function calls are sequenced within the same loop iteration.
- Consider using
std::atomicor RAII scoped guards as alternatives to mutexes, semaphores or queues.
- Tab indentation for HTML, JS, and CSS
- camelCase for JS functions/variables
- Reuse helpers from
common.js— do not duplicate utilities - After editing, run
npm run buildto regenerate headers - Never edit
wled00/html_*.horwled00/js_*.hdirectly
Usermods live in usermods/<name>/ with a .cpp, optional .h, library.json, and readme.md.
class MyUsermod : public Usermod {
private:
bool enabled = false;
static const char _name[];
public:
void setup() override { /* ... */ } // runs once at start-up
void loop() override { /* ... */ } // runs once per main loop iteration
void addToConfig(JsonObject& root) override { /* ... */ } // create/add persistent settings (usermod settings)
bool readFromConfig(JsonObject& root) override { /* ... */ } // read from persistent settings (usermod settings UI)
uint16_t getId() override { return USERMOD_ID_MYMOD; }
void addToJsonInfo(JsonObject& root) override { /* ... */ } // Add custom items to the "info" page and to /json/info
void appendConfigData() override { /* ... */ } // Customize the settings page: dropdowns, checkboxes, extra text, etc. Buffer size is limited!
};
const char MyUsermod::_name[] PROGMEM = "MyUsermod";
static MyUsermod myUsermod;
REGISTER_USERMOD(myUsermod);refer to detailed examples in usermods/EXAMPLE/, usermods/user_fx/ and in the user documentation for custom features.
- Activate via
custom_usermods =in platformio build config. Theusermod_v2_prefix or_v2suffix can be omitted. - Base new usermods on
usermods/EXAMPLE/(never edit the example directly) - Store repeated strings as
static const char[] PROGMEM - Add usermod IDs to
wled00/const.honly when a unique ID is required (see below)
A unique ID (registered in wled00/const.h and overriding getId()) is only required when a usermod needs one or more of the following:
- Inter-usermod communication — another usermod or an FX effect calls
UsermodManager::lookup(mod_id)orUsermodManager::getUMData(..., mod_id)to find or request data from this specific usermod. - Pin ownership via
pinManager— the usermod allocates GPIO pins throughpinManager. Pin ownership is tracked byPinOwnerenum values that map directly toUSERMOD_ID_*constants (seewled00/pin_manager.h). This prevents pin-conflict bugs. - Identification in JSON info —
UsermodManager::addToJsonInfoemits each mod's ID into the"um"array; a unique ID makes the mod identifiable in that output.
If none of the above apply, the usermod may omit getId() (or return the default USERMOD_ID_UNSPECIFIED) and does not need an entry in const.h.
- Called once per main loop iteration. Usermods should simply
returnwhen!enabled. - Frequency of calls varies with system load:
- up to 2000 times/sec with few LEDs and little background activity,
- between 20 and 300 times/second during high workload from effects and other usermods,
- (worst case) down to 1-3 times/sec during FS activity or when serving lots of network API requests.
CI runs on every push/PR via GitHub Actions (.github/workflows/wled-ci.yml):
npm test(web UI build validation)- Firmware compilation for all default environments (~22 targets)
- Post-link validation of usermod linkage (
validate_modules.py)
No automated linting is configured. Match existing code style in files you edit.
- Important: Repository language is English. This applies to source code (including comments), commit messages and any kind of documentation for developer or users.
- The
docs/folder is for developer/contributor information (coding conventions, architecture, etc.). User documentation is maintained in the wled/WLED-Docs repository. - Never edit or commit auto-generated
wled00/html_*.h/wled00/js_*.h. - When updating an existing PR, retain the original description. Only modify it to ensure technical accuracy. Add change logs after the existing description.
- No force-push on open PRs!
- Important: Changes to
platformio.inirequire maintainer approval! - PRs should respect
.gitignoreand not upload files likeplatformio_override.ini. PR authors may add buildenv examples for custom boards intoplatformio_override.ini.sample. - Remove dead/unused code — justify or delete it.
- Verify feature-flag spelling exactly (misspellings are silently ignored by preprocessor).
- Provide references when making analyses or recommendations. Support factual claims with verifiable citations, references or concrete evidence; never fabricate citations.
- Highlight user-visible breaking changes and ripple effects during reviews. Ask for confirmation that these were introduced intentionally.
When writing or reviewing code in wled00/, usermods/, wled00/data/, or .github/workflows/,
consult docs/hardening.instructions.md (concise checklist) and docs/securecode.instructions.md (detailed rules with examples).
These files define WLED's threat model, trust boundary model, and WLED-specific constraints (no TLS baseline, no UDP authentication for protocol-defined
multicast/broadcast, firewall-isolated deployment assumed).
Using AI-generated code can hide the source of the inspiration / knowledge / sources it used.
- Document attribution of inspiration / knowledge / sources used in the code, e.g. link to GitHub repositories or other websites describing the principles / algorithms used.
- When a larger block of code is generated by an AI tool, embed it into
// AI: below section was generated by an AI...// AI: endcomments (see Comments section). - Every non-trivial AI-generated function should have a brief comment describing what it does. Explain parameters when their names alone are not self-explanatory.
- AI-generated code must be well documented with meaningful comments that explain intent, assumptions, and non-obvious logic. Do not rephrase source code; explain concepts and reasoning.
- For "is it worth doing?" debates about proposed reliability, safety, or data-integrity mechanisms (CRC checks, backups, power-loss protection): suggest a software FMEA (Failure Mode and Effects Analysis). Clarify the main feared events, enumerate failure modes, assess each mitigation's effectiveness per failure mode, note common-cause failures, and rate credibility for the typical WLED use case.