Skip to content

Latest commit

 

History

History
332 lines (251 loc) · 21.4 KB

File metadata and controls

332 lines (251 loc) · 21.4 KB

AGENTS.md — WLED AI Coding Agent & AI Code Review Reference

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.

Required User Agreement

Before editing files, committing, pushing, or opening a PR, show the human you're working with this notice and wait for an explicit yes in the current conversation:

WLED welcomes AI-assisted contributions, but not unreviewed ones. I'll only edit, commit, push, or open a PR once you confirm you'll review, understand, and take responsibility for the result. Do you agree?

A generic "go ahead" or silence doesn't count - get a clear yes. Until then, you may read files and answer questions, but do not modify files in the repository or make changes on GitHub.

Build Commands

Note: also read AGENTS.local.md if present (gitignored personal learnings file; it supplements this document).

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.

Running a Single Test

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 directly

There are no C++ unit tests. Firmware is validated by successful compilation across target environments. Always build after code changes: pio run -e esp32dev.

Common Firmware Environments

esp32dev, nodemcuv2, esp8266_2m, esp32c3dev, esp32s3dev_8MB_opi, lolin_s2_mini

Recovery / Troubleshooting

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 deps

Project Structure

wled00/              # 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)

Branch / Release Structure

main                # Main development trunk (daily/nightly) 17.0.0-devV5. Target branch for PRs.
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

C++ Code Style (wled00/, usermods/)

General

  • Follow the existing style in the file you are editing
  • Avoid unexplained "magic numbers". Prefer named constants (constexpr) or C-style #define constants for repeated numbers that have the same meaning
  • If possible, use static for local (C-style) variables and functions (keeps the global namespace clean)
  • When moving global items to another scope, do not leave comments such as // lastMqttReconnectAttempt is now private to wled.cpp - see there. These comments create technical debt for maintenance and will inevitably become out of date

Formatting

  • 2-space indentation (no tabs in C++ files)
  • K&R brace style preferred (opening brace on same line)
  • Single-statement if bodies 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

Comments

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

Naming Conventions

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

Includes

  • 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

Debug Output

  • Use DEBUG_PRINTF() / DEBUG_PRINTLN() / DEBUG_PRINT() for developer diagnostics and debug output (compiled out unless -D WLED_DEBUG)
  • wled00/wled.h defines these macros. They compile to no output when debug output is disabled, keeping normal serial interfaces clean.
  • Do not use direct Serial.print(), Serial.println(), Serial.printf(), or Serial.write() calls unless there is a technical justification for not using DEBUG_... macros.

See docs/cpp.instructions.md section Error Handling for more information.

Types and Const

  • Prefer const & for read-only function parameters
  • Mark getter/query methods const; use static for methods not accessing instance state
  • Prefer constexpr over #define for compile-time constants when possible
  • Use static_assert over #if ... #error
  • Use uint_fast16_t / uint_fast8_t in hot-path code

Error Handling

  • 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() (see previous section)

Strings and Memory

  • Use F("string") for string constants (saves RAM on ESP8266)
  • Use PSTR() with DEBUG_PRINTF_P() for format strings
  • Avoid String in 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

ESP32 PSRAM guidelines

  • Check availability: Test chip availability with psramFound() && ESP.getPsramSize() > 0 before assuming PSRAM is present. Never rely on BOARD_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 when CONFIG_SOC_PSRAM_DMA_CAPABLE is 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 15× 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.
  • Caching files in PSRAM is usually not faster, because flash and PSRAM share the same SPI bus and often run at same speeds. Minor gains are still possible on ESP32-S3 when CONFIG_SOC_PSRAM_DMA_CAPABLE and CONFIG_SPIRAM_MODE_OCT are enabled. Better performance potential is expected on ESP32-P4 with CONFIG_SOC_MEMSPI_FLASH_PSRAM_INDEPENDENT. In special cases, file caching can still help by reducing interrupt contention (for example RMT refill pressure) and preventing visible LED flashing.
  • Consider that ESP32 often crashes when the largest available DRAM chunk gets below 10 KB.

Preprocessor / Feature Flags

  • Feature toggling: WLED_DISABLE_* and WLED_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

Math Functions

  • Use sin8_t(), cos8_t() — NOT sin8(), cos8() (removed, won't compile)
  • Use sin_approx() / cos_approx() instead of sinf() / cosf()
  • Replace inoise8 / inoise16 with perlin8 / perlin16

Hot-Path Code (Pixel Pipeline)

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

ESP32 Tasks

  • delay(1) in custom FreeRTOS tasks (NOT yield()) — feeds IDLE watchdog
  • Do not use delay() in effects (FX.cpp) or hot pixel path

ESP32 Task Synchronization

  • 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.
  • Don't use portMAX_DELAY when waiting to acquire a mutex - this can lock the task indefinitely. Find a reasonable max waiting time, and handle mutex timeouts gracefully.
  • 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::atomic or RAII scoped guards as alternatives to mutexes, semaphores or queues.

Web UI Code Style (wled00/data/)

  • 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 build to regenerate headers
  • Never edit wled00/html_*.h or wled00/js_*.h directly

Usermods

Source Code Location

Usermod Pattern

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. The usermod_v2_ prefix or _v2 suffix 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.h only when a unique ID is required (see below)

Pin ownership via pinManager

  • Before performing any operation on I/O pins, the usermod must allocate its pins from the pinManager.
  • I/O pins are allocated via PinManager::allocatePin(byte gpio, bool output, PinOwner tag) (or PinManager::allocateMultiplePins()), and returned via PinManager::deallocatePin() when re-configuring pin numbers.
  • PinManager::allocatePin() will return an error code in case that a pin is already assigned to another WLED function.
  • You can use -1 = 255 for unconfigured / unassigned pin functions.
  • Check for valid pin numbers with PinManager::isPinOk(byte gpio, bool output); this function knows which GPI(O) pins are possible on your specific MCU.
  • PIN numbers for I2C and SPI busses are configured globally in the generic part of the usermod settings page.
  • Usually, a usermod will not start the I2C or SPI units explicitly, since they are already initialised when WLED starts.
  • Use this pattern to check for an incomplete I2C setup: if (i2c_scl<0 || i2c_sda<0) {enabled = false; return;}.

Usermod IDs

A unique ID (registered in wled00/const.h and overriding getId()) is only required when a usermod needs one or more of the following:

  1. Inter-usermod communication — another usermod or an FX effect calls UsermodManager::lookup(mod_id) or UsermodManager::getUMData(..., mod_id) to find or request data from this specific usermod.
  2. Pin ownership via PinManager — the usermod allocates GPIO pins through pinManager. Pin ownership is tracked by PinOwner enum values that map directly to USERMOD_ID_* constants (see wled00/pin_manager.h). This prevents pin-conflict bugs.
  3. Identification in JSON info — UsermodManager::addToJsonInfo emits 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.

Usermod loop()

  • Called once per main loop iteration. Usermods should simply return when !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.

See Also

CI/CD

CI runs on every push/PR via GitHub Actions (.github/workflows/wled-ci.yml):

  1. npm test (web UI build validation)
  2. Firmware compilation for all default environments (~22 targets)
  3. Post-link validation of usermod linkage (validate_modules.py)

No automated linting is configured. Match existing code style in files you edit.

General Rules

  • Important: Repository language is English. This applies to source code (including comments), commit messages and any kind of documentation for developer or users.
  • Provide references when making analyses or recommendations. Support factual claims with verifiable citations, references or concrete evidence; never fabricate citations.
  • 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.
  • Remove dead/unused code — justify or delete it.
  • Verify feature-flag spelling exactly (misspellings are silently ignored by preprocessor).
  • Important: Changes to platformio.ini require maintainer approval!
  • PRs should respect .gitignore and not upload files like platformio_override.ini. PR authors may add buildenv examples for custom boards into platformio_override.ini.sample.
  • Highlight user-visible breaking changes and ripple effects during reviews.
  • When updating an existing PR, retain the original description. Only modify it to ensure technical accuracy. Add change logs after the existing description.

Pull Request Expectations

  • No force-push on open PRs!
  • Every pull request needs a clear description of what changed and why. If the change affects user-visible behavior, describe the expected impact. Link to related issues where applicable.
  • Best practice: Consider adding screenshots to showcase new features.
  • Do not prefix the PR title with fix:, feat: or other keywords meant to define the type of PR. Use combinations of labels (bug, enhancement, effect, usermod, slop, etc.) instead.
  • Important: Fully or partially AI coded PRs MUST be declared clearly in the description - in addition to comments markers in the source code (see Comments section).

Security Hardening

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

Attribution for AI-generated code

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: end comments (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.

Guardrails for Coding Agents

These apply to any coding agent (Copilot or otherwise) acting on a contributor's behalf in this repository:

  • Confirm before you commit. Make sure the human understands and can explain the change - especially root-cause claims or refactors - before you open a PR.
  • Stay scoped, but don't dodge architecture. Touch only what the task requires, and flag broader fixes to the human instead of silently expanding the change. But don't use "keep it minimal" as an excuse to avoid a needed core-level discussion - if the clean fix requires a new core capability or interface, say so and propose it, rather than bolting on a weak hook, monkey-patch, or board-specific workaround just to keep the diff small.
  • Warning: avoid overconfident root-cause claims. When drafting issue tickets or PR descriptions, don't state a root cause with confidence unless you've actually traced the code path or reproduced the failure. A plausible-sounding explanation is not the same as a verified one - overconfident claims that later "dissolve into nothing" under review waste maintainer time and damage trust in the whole PR. When in doubt, say so explicitly.
  • State what's tested. Distinguish "verified" from "should work based on reading the code" in the PR description.
  • Disclose AI involvement. Declare AI assistance in the PR description.
  • Let issues start non-technical. When helping draft an issue or feature request, elicit the human's objective or observation in plain language first - don't lead with a proposed code path or fix. Maintainers are better positioned than you to map that objective onto a technical solution.

Supporting Reviews and Discussions

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