diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 0000000000..5be93fa226 --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,113 @@ +# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json +# +# CodeRabbit configuration — references existing guideline files to avoid +# duplicating conventions. See: +# .github/copilot-instructions.md — project overview & general rules +# docs/cpp.instructions.md — C++ coding conventions +# docs/web.instructions.md — Web UI coding conventions +# docs/cicd.instructions.md — GitHub Actions / CI-CD conventions +# +# NOTE: This file must be committed (tracked by git) for CodeRabbit to read +# it from the repository. If it is listed in .gitignore, CodeRabbit will +# not see it and these settings will have no effect. + +language: en-US + +reviews: + # generic review setting, see https://docs.coderabbit.ai/reference/configuration#reference + auto_apply_labels: true + # abort_on_close: false + high_level_summary: true + review_status: true + collapse_walkthrough: false + poem: false + # sequence_diagrams: false + auto_review: + enabled: true + ignore_title_keywords: + - WIP + + path_instructions: + - path: "**/*.{cpp,h,hpp,ino}" + instructions: > + Follow the C++ coding conventions documented in docs/cpp.instructions.md + and the general project guidelines in .github/copilot-instructions.md. + + Key rules: 2-space indentation (no tabs), camelCase functions/variables, + PascalCase classes, UPPER_CASE macros. No C++ exceptions — use return codes and debug macros. + + Hot-path optimization guidelines (attributes, uint_fast types, caching, + unsigned range checks) apply from pixel set/get operations and strip.show() downward — + NOT to effect functions in FX.cpp, which have diverse contributor styles. +# disabled - the below instruction has no effect +# When initially reviewing a PR, summarize good practices (top 5) and create a prioritized list of suggested improvements (focus on major ones). + + - path: "wled00/data/**" + instructions: > + Follow the web UI conventions documented in docs/web.instructions.md. + + Key rules: indent HTML and JavaScript with tabs, CSS with tabs. + Files here are built into wled00/html_*.h and wled00/js_*.h by tools/cdata.js — never + edit those generated headers directly. +# disabled - the below instruction has no effect +# When initially reviewing a PR, summarize good practices (top 5) and create a prioritized list of suggested improvements (focus on major ones). + + - path: "wled00/html_*.h" + instructions: > + These files are auto-generated from wled00/data/ by tools/cdata.js. + They must never be manually edited or committed. Flag any PR that + includes changes to these files. + + - path: "wled00/js_*.h" + instructions: > + These files are auto-generated from wled00/data/ by tools/cdata.js. + They must never be manually edited or committed. Flag any PR that + includes changes to these files. + + - path: "usermods/**" + instructions: > + Usermods are community add-ons. + Each usermod lives in its own directory under usermods/ and is implemented + as a .cpp file with a dedicated library.json file to manage dependencies. + Follow the same C++ conventions as the core firmware (docs/cpp.instructions.md). +# disabled - the below instruction has no effect +# When initially reviewing a PR, summarize good practices (top 6) and create a prioritized list of suggested improvements (skip minor ones). + + - path: ".github/workflows/*.{yml,yaml}" + instructions: > + Follow the CI/CD conventions documented in docs/cicd.instructions.md. + + Key rules: 2-space indentation, descriptive name: on every workflow/job/step. + Third-party actions must be pinned to a specific version tag — branch pins + such as @main or @master are not allowed. Declare explicit permissions: blocks + scoped to least privilege. Never interpolate github.event.* values directly + into run: steps — pass them through an env: variable to prevent script + injection. Do not use pull_request_target unless fully justified. +# disabled - the below instruction has no effect +# When initially reviewing a PR, summarize good practices (top 6) and create a prioritized list of suggested improvements. + + - path: "**/*.instructions.md" + instructions: | + This file contains both AI-facing rules and human-only reference sections. + Human-only sections are enclosed in `` / + `` HTML comment markers and should not be used as + actionable review criteria. + + When this file is modified in a PR, perform the following alignment check: + 1. For each ` ... ` block, + verify that its examples and guidance are consistent with (and do not + contradict) the AI-facing rules stated in the same file. + 2. Flag any HUMAN_ONLY section whose content has drifted from the surrounding + AI-facing rules due to edits introduced in this PR. + 3. If new AI-facing rules were added without updating a related HUMAN_ONLY + reference section, note this as a suggestion (not a required fix). + + finishing_touches: + # Docstrings | Options for generating Docstrings for your PRs/MRs. + docstrings: + # Docstrings | Allow CodeRabbit to generate docstrings for PRs/MRs. + # default: true - disabled in WLED: has caused confusion in the past + enabled: false + unit_tests: + # default: true - disabled in WLED: we don't have a unit test framework, this option just confuses contributors + enabled: false diff --git a/.github/agent-build.instructions.md b/.github/agent-build.instructions.md new file mode 100644 index 0000000000..c7854fad16 --- /dev/null +++ b/.github/agent-build.instructions.md @@ -0,0 +1,120 @@ +--- +applyTo: "**" +--- +# Agent-Mode Build & Test Instructions + +Detailed build workflow, timeouts, and troubleshooting for making code changes in agent mode. Always reference these instructions first when running builds or validating changes. + +## Build Timing and Timeouts + +Use these timeout values when running builds: + +| Command | Typical Time | Minimum Timeout | Notes | +|---|---|---|---| +| `npm run build` | ~3 s | 30 s | Web UI → `wled00/html_*.h` `wled00/js_*.h` headers | +| `npm test` | ~40 s | 2 min | Validates build system | +| `npm run dev` | continuous | — | Watch mode, auto-rebuilds on changes | +| `pio run -e ` | 15–20 min | 30 min | First build downloads toolchains; subsequent builds are faster | + +**NEVER cancel long-running builds.** PlatformIO downloads and compilation require patience. + +## Development Workflow + +### Code Style Summary +- **C++** files in `wled00/` and `usermods/`: 2-space indentation (no tabs), camelCase functions/variables, PascalCase classes, UPPER_CASE macros. No C++ exceptions — use return codes and debug macros. +- **Web UI** files in `wled00/data`: indent HTML and JavaScript with tabs, CSS with tabs. +- **CI/CD workflows** in `.github/workflows`: 2-space indentation, descriptive `name:` on every workflow/job/step. Third-party actions must be pinned to a specific version tag — branch pins such as `@main` or `@master` are not allowed. SHA pinning recommended. + +### Web UI Changes + +1. Edit files in `wled00/data/` +2. Run `npm run build` to regenerate `wled00/html_*.h` `wled00/js_*.h` headers +3. Test with local HTTP server (see Manual Testing below) +4. Run `npm test` to validate + +### Firmware Changes + +1. Edit files in `wled00/` (but **never** `html_*.h` and `js_*.h` files) +2. Ensure web UI is built first: `npm run build` +3. Build firmware: `pio run -e esp32dev` (set timeout ≥ 30 min) +4. Flash to device: `pio run -e [target] --target upload` + +### Combined Web + Firmware Changes + +1. Always build web UI first +2. Test web interface manually +3. Then build and test firmware + + +## Before Finishing Work - Testing + +**You MUST complete ALL of these before marking work as done:** + +1. **Run tests**: `npm test` — must pass +2. **Build firmware**: `pio run -e esp32dev` — must succeed after source code changes, **never skip this step**. + - Set timeout to 30+ minutes, **never cancel** + - Choose `esp32dev` as a common, representative environment + - If the build fails, fix the issue before proceeding +3. **For web UI changes**: manually test the interface (see below) + +If any step fails, fix the issue. **Do NOT mark work complete with failing builds or tests.** + +## Manual Web UI Testing + +Start a local server: + +```sh +cd wled00/data && python3 -m http.server 8080 +# Open http://localhost:8080/index.htm +``` + +Test these scenarios after every web UI change: + +- **Load**: `index.htm` loads without JavaScript errors (check browser console) +- **Navigation**: switching between main page and settings pages works +- **Color controls**: color picker and brightness controls function correctly +- **Effects**: effect selection and parameter changes work +- **Settings**: form submission and validation work + +## Troubleshooting + +### Common Build Issues + +| Problem | Solution | +|---|---| +| Missing `html_*.h` | Run `npm ci; npm run build` | +| Web UI looks broken | Check browser console for JS errors | +| PlatformIO network errors | Retry — downloads can be flaky | +| Node.js version mismatch | Ensure Node.js 20+ (check `.nvmrc`) | + +### Recovery Steps + +- **Force web UI rebuild**: `npm run build -- -f` +- **Clear generated files**: `rm -f wled00/html_*.h wled00/js_*.h` then `npm run build` +- **Clean PlatformIO build artifacts**: `pio run --target clean` +- **Reinstall Node deps**: `rm -rf node_modules && npm ci` + +## CI/CD Validation + +The GitHub Actions CI workflow will: +1. Install Node.js and Python dependencies +2. Run `npm test` +3. Build web UI (automatic via PlatformIO) +4. Compile firmware for **all** `default_envs` targets + +**To ensure CI success, always validate locally:** +- Run `npm test` and ensure it passes +- Run `pio run -e esp32dev` (or another common firmware environment, see next section) and ensure it completes successfully +- If either fails locally, it WILL fail in CI + +Match this workflow in local development to catch failures before pushing. + +## Important Reminders + +- Always **commit source code** +-  Every pull request MUST include a clear description of *what* changed and *why*. +- **Never edit or commit** `wled00/html_*.h` and `wled00/js_*.h` — auto-generated from `wled00/data/` +- After modifying source code files, check that any **previous comments have been preserved** or updated to reflect the new behaviour. +- Web UI rebuild is part of the PlatformIO firmware compilation pipeline +- Common environments: `nodemcuv2`, `esp32dev`, `esp8266_2m`, `esp32c3dev`, `esp32s3dev_8MB_opi` +- List all PlatformIO targets: `pio run --list-targets` diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 3a2cc14554..0360a209d9 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -4,175 +4,136 @@ WLED is a fast and feature-rich implementation of an ESP32 and ESP8266 webserver Always reference these instructions first and fallback to search or bash commands only when you encounter unexpected information that does not match the info here. -## Working Effectively - -### Initial Setup -- Install Node.js 20+ (specified in `.nvmrc`): Check your version with `node --version` -- Install dependencies: `npm ci` (takes ~5 seconds) -- Install PlatformIO for hardware builds: `pip install -r requirements.txt` (takes ~60 seconds) - -### Build and Test Workflow -- **ALWAYS build web UI first**: `npm run build` -- takes 3 seconds. NEVER CANCEL. -- **Run tests**: `npm test` -- takes 40 seconds. NEVER CANCEL. Set timeout to 2+ minutes. -- **Development mode**: `npm run dev` -- monitors file changes and auto-rebuilds web UI -- **Hardware firmware build**: `pio run -e [environment]` -- takes 15+ minutes. NEVER CANCEL. Set timeout to 30+ minutes. - -### Build Process Details -The build has two main phases: -1. **Web UI Generation** (`npm run build`): - - Processes files in `wled00/data/` (HTML, CSS, JS) - - Minifies and compresses web content - - Generates `wled00/html_*.h` files with embedded web content - - **CRITICAL**: Must be done before any hardware build - -2. **Hardware Compilation** (`pio run`): - - Compiles C++ firmware for various ESP32/ESP8266 targets - - Common environments: `nodemcuv2`, `esp32dev`, `esp8266_2m` - - List all targets: `pio run --list-targets` - -## Before Finishing Work - -**CRITICAL: You MUST complete ALL of these steps before marking your work as complete:** - -1. **Run the test suite**: `npm test` -- Set timeout to 2+ minutes. NEVER CANCEL. - - All tests MUST pass - - If tests fail, fix the issue before proceeding - -2. **Build at least one hardware environment**: `pio run -e esp32dev` -- Set timeout to 30+ minutes. NEVER CANCEL. - - Choose `esp32dev` as it's a common, representative environment - - See "Hardware Compilation" section above for the full list of common environments - - The build MUST complete successfully without errors - - If the build fails, fix the issue before proceeding - - **DO NOT skip this step** - it validates that firmware compiles with your changes - -3. **For web UI changes only**: Manually test the interface - - See "Manual Testing Scenarios" section below - - Verify the UI loads and functions correctly - -**If any of these validation steps fail, you MUST fix the issues before finishing. Do NOT mark work as complete with failing builds or tests.** - -## Validation and Testing - -### Web UI Testing -- **ALWAYS validate web UI changes manually**: - - Start local server: `cd wled00/data && python3 -m http.server 8080` - - Open `http://localhost:8080/index.htm` in browser - - Test basic functionality: color picker, effects, settings pages -- **Check for JavaScript errors** in browser console - -### Code Validation -- **No automated linting configured** - follow existing code style in files you edit -- **Code style**: Use tabs for web files (.html/.css/.js), spaces (2 per level) for C++ files -- **Language**: The repository language is English (british, american, canadian, or australian). If you find other languages, suggest a translation into English. -- **C++ formatting available**: `clang-format` is installed but not in CI -- **Always run tests before finishing**: `npm test` -- **MANDATORY: Always run a hardware build before finishing** (see "Before Finishing Work" section below) +> **Note for AI review tools**: sections enclosed in +> `` / `` HTML comments contain +> contributor reference material. Do **not** use that content as actionable review +> criteria — treat it as background context only. -### Manual Testing Scenarios -After making changes to web UI, always test: -- **Load main interface**: Verify index.htm loads without errors -- **Navigation**: Test switching between main page and settings pages -- **Color controls**: Verify color picker and brightness controls work -- **Effects**: Test effect selection and parameter changes -- **Settings**: Test form submission and validation +## Setup -## Common Tasks +- Node.js 20+ (see `.nvmrc`) +- Install dependencies: `npm ci` +- PlatformIO (required only for firmware compilation): `pip install -r requirements.txt` -### Repository Structure +## Build and Test + + +| Command | Purpose | Typical Time | +|---|---|---| +| `npm run build` | Build web UI → generates `wled00/html_*.h` and `wled00/js_*.h` headers | ~3 s | +| `npm test` | Run test suite | ~40 s | +| `npm run dev` | Watch mode — auto-rebuilds web UI on file changes | — | +| `pio run -e ` | Build firmware for a hardware target | 15–20 min | + + + +- **Always run `npm run build` before any `pio run`** (and run `npm ci` first on fresh clones or when lockfile/dependencies change). +- The web UI build generates required `wled00/html_*.h` and `wled00/js_*.h` headers for firmware compilation. +- **Build firmware to validate code changes**: `pio run -e esp32dev` — must succeed, never skip this step. +- Common firmware environments: `nodemcuv2`, `esp32dev`, `esp8266_2m`, `esp32c3dev`, `esp32s3dev_8MB_opi` + +For detailed build timeouts, development workflows, troubleshooting, and validation steps, see [agent-build.instructions.md](agent-build.instructions.md). + +### Usermod Guidelines + + - New custom effects can be added into the user_fx usermod. Read the [user_fx documentation](https://github.com/wled/WLED/blob/main/usermods/user_fx/README.md) for guidance. + - Other usermods may be based on the [EXAMPLE usermod](https://github.com/wled/WLED/tree/main/usermods/EXAMPLE). Never edit the example, always create a copy! + - New usermod IDs can be added into [wled00/const.h](https://github.com/wled/WLED/blob/main/wled00/const.h#L160). + - To activate a usermod, a custom build configuration should be used. Add the usermod name to `custom_usermods`. + +## Project Structure Overview + +### Project Branch / Release Structure + + +```text +main # Main development trunk (daily/nightly) 17.0.0-dev + ├── 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 # current beta, preparations for next release 16.0.0 +0_15_x # maintenance (bugfixes only) for current release 0.15.4 +(tag) v0.14.4 # previous 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 ``` -wled00/ # Main firmware source (C++) - ├── data/ # Web interface files - │ ├── index.htm # Main UI + + +- ``main``: development trunk (daily/nightly) +- ``V5`` and ``V5-C6``: code rework for esp-idf 5.5.x (unstable) - branched from ``main``. +- ``0_15_x``: bugfixing / maintenance for release 0.15.x + +### Repository Structure + +tl;dr: +* Firmware source: `wled00/` (C++). Web UI source: `wled00/data/`. Build targets: `platformio.ini`. +* Auto-generated headers: `wled00/html_*.h` and `wled00/js_*.h` — **never edit or commit**. +* ArduinoJSON + AsyncJSON: `wled00/src/dependencies/json` (included via `wled.h`). CI/CD: `.github/workflows/`. +* Usermods: `usermods/` (C++, with individual library.json). +* Contributor docs: `docs/` (coding guidelines, etc). + + +Detailed overview: + +```text +wled00/ # Main firmware source (C++) "WLED core" + ├── data/ # Web interface files + │ ├── index.htm # Main UI │ ├── settings*.htm # Settings pages - │ └── *.js/*.css # Frontend resources - ├── *.cpp/*.h # Firmware source files - └── html_*.h # Auto-generated embedded web files (DO NOT EDIT, DO NOT COMMIT) -tools/ # Build tools (Node.js) + │ └── *.js/*.css # Frontend resources + ├── *.cpp/*.h # Firmware source files + ├── html_*.h # Auto-generated embedded web files (DO NOT EDIT, DO NOT COMMIT) + ├── src/ # Modules used by the WLED core (C++) + │ ├── fonts/ # Font libraries for scrolling text effect + └ └── dependencies/ # Utility functions - some of them have their own licensing terms +lib/ # Project specific custom libraries. PlatformIO will compile them to separate static libraries and link them +platformio.ini # Hardware build configuration + +platformio_override.sample.ini # examples for custom build configurations - entries must be copied into platformio_override.ini to use them. + # platformio_override.ini is _not_ stored in the WLED repository! +usermods/ # User-contributed addons to the WLED core, maintained by individual contributors (C++, with individual library.json) +package.json # Node.js dependencies and scripts, release identification +pio-scripts/ # Build tools (PlatformIO) +tools/ # Build tools (Node.js), partition files, and generic utilities ├── cdata.js # Web UI build script └── cdata-test.js # Test suite -platformio.ini # Hardware build configuration -package.json # Node.js dependencies and scripts +docs/ # Contributor docs, coding guidelines .github/workflows/ # CI/CD pipelines ``` -### Key Files and Their Purpose -- `wled00/data/index.htm` - Main web interface -- `wled00/data/settings*.htm` - Configuration pages -- `tools/cdata.js` - Converts web files to C++ headers -- `wled00/wled.h` - Main firmware configuration -- `platformio.ini` - Hardware build targets and settings - -### Development Workflow (applies to agent mode only) -1. **For web UI changes**: - - Edit files in `wled00/data/` - - Run `npm run build` to regenerate headers - - Test with local HTTP server - - Run `npm test` to validate build system - -2. **For firmware changes**: - - Edit files in `wled00/` (but NOT `html_*.h` files) - - Ensure web UI is built first (`npm run build`) - - Build firmware: `pio run -e [target]` - - Flash to device: `pio run -e [target] --target upload` - -3. **For both web and firmware**: - - Always build web UI first - - Test web interface manually - - Build and test firmware if making firmware changes - -## Build Timing and Timeouts - -**IMPORTANT: Use these timeout values when running builds:** - -- **Web UI build** (`npm run build`): 3 seconds typical - Set timeout to 30 seconds minimum -- **Test suite** (`npm test`): 40 seconds typical - Set timeout to 120 seconds (2 minutes) minimum -- **Hardware builds** (`pio run -e [target]`): 15-20 minutes typical for first build - Set timeout to 1800 seconds (30 minutes) minimum - - Subsequent builds are faster due to caching - - First builds download toolchains and dependencies which takes significant time -- **NEVER CANCEL long-running builds** - PlatformIO downloads and compilation require patience - -**When validating your changes before finishing, you MUST wait for the hardware build to complete successfully. Set the timeout appropriately and be patient.** - -## Troubleshooting - -### Common Issues -- **Build fails with missing html_*.h**: Run `npm run build` first -- **Web UI looks broken**: Check browser console for JavaScript errors -- **PlatformIO network errors**: Try again, downloads can be flaky -- **Node.js version issues**: Ensure Node.js 20+ is installed (check `.nvmrc`) - -### When Things Go Wrong -- **Clear generated files**: `rm -f wled00/html_*.h` then rebuild -- **Force web UI rebuild**: `npm run build -- --force` or `npm run build -- -f` -- **Clean PlatformIO cache**: `pio run --target clean` -- **Reinstall dependencies**: `rm -rf node_modules && npm install` - -## Important Notes - -- **Always commit source files** -- **Web UI re-built is part of the platformio firmware compilation** -- **do not commit generated html_*.h files** -- **DO NOT edit `wled00/html_*.h` files** - they are auto-generated. If needed, modify Web UI files in `wled00/data/`. -- **Test web interface manually after any web UI changes** -- When reviewing a PR: the PR author does not need to update/commit generated html_*.h files - these files will be auto-generated when building the firmware binary. -- **If you are not sure about something, just answer that you are not sure.** Gather more information instead of continuing with a wild guess. -- If asked for an analysis, assessment or web research, **always provide references** to justify your conclusions. -- If updating Web UI files in `wled00/data/`, make use of common functions availeable in `wled00/data/common.js` where possible. -- **Use VS Code with PlatformIO extension for best development experience** -- **Hardware builds require appropriate ESP32/ESP8266 development board** - -## CI/CD Pipeline - -**The GitHub Actions CI workflow will:** -1. Installs Node.js and Python dependencies -2. Runs `npm test` to validate build system (MUST pass) -3. Builds web UI with `npm run build` (automatically run by PlatformIO) -4. Compiles firmware for ALL hardware targets listed in `default_envs` (MUST succeed for all) -5. Uploads build artifacts - -**To ensure CI success, you MUST locally:** -- Run `npm test` and ensure it passes -- Run `pio run -e esp32dev` (or another common environment from "Hardware Compilation" section) and ensure it completes successfully -- If either fails locally, it WILL fail in CI - -**Match this workflow in your local development to ensure CI success. Do not mark work complete until you have validated builds locally.** + +## General Guidelines + +- **Repository language is English.** Suggest translations for non-English content. +- **Use VS Code with PlatformIO extension** for best development experience. +- **Never edit or commit** `wled00/html_*.h` and `wled00/js_*.h` — auto-generated from `wled00/data/`. +- If updating Web UI files in `wled00/data/`, **make use of common functions in `wled00/data/common.js` whenever possible**. +- **When unsure, say so.** Gather more information rather than guessing. +- **Acknowledge good patterns** when you see them. Positive feedback always helps. +- **Provide references** when making analyses or recommendations. Base them on the correct branch or PR. +- **Highlight user-visible breaking changes and ripple effects**. Ask for confirmation that these were introduced intentionally. +- **Unused / dead code must be justified or removed**. This helps to keep the codebase clean, maintainable and readable. +- **Verify feature-flag names.** Every `WLED_ENABLE_*` / `WLED_DISABLE_*` flag must exactly match one of the names below — misspellings are silently ignored by the preprocessor (e.g. `WLED_IR_DISABLE` instead of `WLED_DISABLE_INFRARED`), causing silent build variations. Flag unrecognised names as likely typos and suggest the correct spelling. +
**`WLED_DISABLE_*`**: `2D`, `ADALIGHT`, `ALEXA`, `BROWNOUT_DET`, `ESPNOW`, `FILESYSTEM`, `HUESYNC`, `IMPROV_WIFISCAN`, `INFRARED`, `LOXONE`, `MQTT`, `OTA`, `PARTICLESYSTEM1D`, `PARTICLESYSTEM2D`, `PIXELFORGE`, `WEBSOCKETS` +
**`WLED_ENABLE_*`**: `ADALIGHT`, `AOTA`, `DMX`, `DMX_INPUT`, `DMX_OUTPUT`, `FS_EDITOR`, `GIF`, `HUB75MATRIX`, `JSONLIVE`, `LOXONE`, `MQTT`, `PIXART`, `PXMAGIC`, `USERMOD_PAGE`, `WEBSOCKETS`, `WPA_ENTERPRISE` +- **C++ formatting available**: `clang-format` is installed but not in CI +- No automated linting is configured — match existing code style in files you edit. + +Refer to `docs/cpp.instructions.md` and `docs/web.instructions.md` for language-specific conventions, and `docs/cicd.instructions.md` for GitHub Actions workflows. + +### 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 C++ guidelines). +- 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. + +### Pull Request Expectations + +- **No force-push on open PRs.** Once a pull request is open and being reviewed, do not force-push (`git push --force`) to the branch. Force-pushing rewrites history that reviewers may have already commented on, making it impossible to track incremental changes. Use regular commits or `git merge` to incorporate feedback; the branch will be squash-merged when it is accepted. +- **Modifications to ``platformio.ini`` MUST be approved explicitly** by a *maintainer* or *WLED organisation Member*. Modifications to the global build environment may break github action builds. Always flag them. +- **Document your changes in the PR.** Every pull request should include 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. Provide screenshots to showcase new features. + +### 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. + diff --git a/.github/platformio_release.ini.template b/.github/platformio_release.ini.template new file mode 100644 index 0000000000..499243822b --- /dev/null +++ b/.github/platformio_release.ini.template @@ -0,0 +1,44 @@ +; ---------------------------------------------------------------------------- +; platformio_release.ini.template +; ---------------------------------------------------------------------------- +; Copied to platformio_release.ini by the release CI workflow +; (.github/workflows/release.yml -> build.yml with `release: true`) +; in order to extend the matrix of `default_envs` built and published +; for tagged releases. +; +; This file overrides `[platformio].default_envs` from platformio.ini via +; `extra_configs`. It MUST list every env that should be released - including +; the regular CI default_envs - because it fully replaces the parent value. +; +; Do NOT commit a generated platformio_release.ini (it's in .gitignore). +; ---------------------------------------------------------------------------- + +[platformio] +default_envs = nodemcuv2 + esp8266_2m + esp01_1m_full + nodemcuv2_160 + esp8266_2m_160 + esp01_1m_full_160 + nodemcuv2_compat + esp8266_2m_compat + esp01_1m_full_compat + esp32dev + esp32dev_debug + esp32_eth + esp32_wrover + lolin_s2_mini + esp32c3dev + esp32c3dev_qio + esp32S3_wroom2 + esp32s3dev_16MB_opi + esp32s3dev_8MB_opi + esp32s3dev_8MB_qspi + esp32s3_4M_qspi + usermods + ; HUB75 release-only envs + esp32dev_hub75 + esp32dev_hub75_forum_pinout + adafruit_matrixportal_esp32s3 + esp32s3dev_16MB_opi_hub75 ;; MoonHub + esp32s3dev_4MB_qspi_hub75 ;; HD-WF2 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f0d8537035..fc3003358d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,7 +3,15 @@ name: WLED Build # Only included into other workflows on: workflow_call: - + inputs: + release: + description: 'Build the release env matrix (uses .github/platformio_release.ini.template)' + type: boolean + default: false + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + jobs: get_default_envs: @@ -11,6 +19,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - name: Apply release config + if: inputs.release + run: cp .github/platformio_release.ini.template platformio_release.ini - uses: actions/setup-python@v5 with: python-version: '3.12' @@ -35,6 +46,9 @@ jobs: environment: ${{ fromJSON(needs.get_default_envs.outputs.environments) }} steps: - uses: actions/checkout@v4 + - name: Apply release config + if: inputs.release + run: cp .github/platformio_release.ini.template platformio_release.ini - name: Set up Node.js uses: actions/setup-node@v4 with: @@ -51,8 +65,8 @@ jobs: ~/.platformio/.cache ~/.buildcache build_output - key: pio-${{ runner.os }}-${{ matrix.environment }}-${{ hashFiles('platformio.ini', 'pio-scripts/output_bins.py') }}-${{ hashFiles('wled00/**', 'usermods/**') }} - restore-keys: pio-${{ runner.os }}-${{ matrix.environment }}-${{ hashFiles('platformio.ini', 'pio-scripts/output_bins.py') }}- + key: pio-${{ runner.os }}-${{ matrix.environment }}-${{ hashFiles('platformio.ini', '.github/platformio_release.ini.template', 'pio-scripts/output_bins.py') }}-${{ hashFiles('wled00/**', 'usermods/**') }} + restore-keys: pio-${{ runner.os }}-${{ matrix.environment }}-${{ hashFiles('platformio.ini', '.github/platformio_release.ini.template', 'pio-scripts/output_bins.py') }}- - name: Set up Python uses: actions/setup-python@v5 with: @@ -63,9 +77,21 @@ jobs: - name: Build firmware run: pio run -e ${{ matrix.environment }} + - name: Get artifact name from bin filename + id: artifact_name + run: | + bin=$(ls build_output/release/*.bin 2>/dev/null | head -1) + if [ -n "$bin" ]; then + # Strip WLED__ prefix from WLED__.bin + base=$(basename "$bin" .bin) + release_name=$(echo "$base" | sed 's/^[^_]*_[^_]*_//') + echo "name=firmware-$release_name" >> $GITHUB_OUTPUT + else + echo "name=firmware-${{ matrix.environment }}" >> $GITHUB_OUTPUT + fi - uses: actions/upload-artifact@v4 with: - name: firmware-${{ matrix.environment }} + name: ${{ steps.artifact_name.outputs.name }} path: | build_output/release/*.bin build_output/release/*_ESP02*.bin.gz diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 2d47aefe42..3819bf66da 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -7,6 +7,9 @@ on: # This can be used to allow manually triggering nightlies from the web interface workflow_dispatch: +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + jobs: wled_build: uses: ./.github/workflows/build.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 59de4316cb..1ad7cfb4c6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,10 +5,15 @@ on: tags: - '*' +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + jobs: wled_build: uses: ./.github/workflows/build.yml + with: + release: true release: name: Create Release @@ -18,6 +23,14 @@ jobs: - uses: actions/download-artifact@v4 with: merge-multiple: true + - name: Create draft release + uses: softprops/action-gh-release@v1 + with: + body: "Release assets uploaded. Release notes pending..." + draft: True + files: | + *.bin + *.bin.gz - name: "✏️ Generate release changelog" id: changelog uses: janheinrichmerker/action-github-changelog-generator@v2.4 @@ -27,12 +40,9 @@ jobs: maxIssues: 500 # Exclude issues that were closed without resolution from changelog excludeLabels: 'stale,wontfix,duplicate,invalid,external,question,use-as-is,not_planned' - - name: Create draft release + - name: Update release description uses: softprops/action-gh-release@v1 with: body: ${{ steps.changelog.outputs.changelog }} draft: True - files: | - *.bin - *.bin.gz diff --git a/.github/workflows/usermods.yml b/.github/workflows/usermods.yml index e8ab65066d..24eda32ece 100644 --- a/.github/workflows/usermods.yml +++ b/.github/workflows/usermods.yml @@ -4,7 +4,10 @@ on: pull_request: paths: - usermods/** - + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + jobs: get_usermod_envs: diff --git a/.gitignore b/.gitignore index 62e72a9a0a..a2f10883a6 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ __pycache__/ esp01-update.sh platformio_override.ini +platformio_release.ini replace_fs.py wled-update.sh diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..634fbb2176 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,184 @@ +# AGENTS.md — WLED Coding Agent 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`. + +## Build Commands + +| 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) | 30 min | +| `pio run -e nodemcuv2` | Build firmware (ESP8266) | 30 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: + +```sh +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 + +```sh +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) +``` + +## C++ Code Style (wled00/, usermods/) + +### 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 + +### 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` + +### 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()` (compiled out unless `-D WLED_DEBUG`) + +### 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 + +### 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` + +### Comments +- `//` for inline (always space after), `/* */` for block comments +- AI-generated blocks: mark with `// AI: below section was generated by an AI` / `// AI: end` + +### 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 + +## 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 + +## Usermod Pattern + +Usermods live in `usermods//` with a `.cpp`, optional `.h`, `library.json`, and `readme.md`. + +```cpp +class MyUsermod : public Usermod { + private: + bool enabled = false; + static const char _name[]; + public: + void setup() override { /* ... */ } + void loop() override { /* ... */ } + void addToConfig(JsonObject& root) override { /* ... */ } + bool readFromConfig(JsonObject& root) override { /* ... */ } + uint16_t getId() override { return USERMOD_ID_MYMOD; } +}; +const char MyUsermod::_name[] PROGMEM = "MyUsermod"; +static MyUsermod myUsermod; +REGISTER_USERMOD(myUsermod); +``` + +- Add usermod IDs to `wled00/const.h` +- Activate via `custom_usermods` in platformio build config +- Base new usermods on `usermods/EXAMPLE/` (never edit the example directly) +- Store repeated strings as `static const char[] PROGMEM` + +## 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 + +- Repository language is English +- 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 +- Changes to `platformio.ini` require maintainer approval +- Remove dead/unused code — justify or delete it +- Verify feature-flag spelling exactly (misspellings are silently ignored by preprocessor) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1cd77ecd14..91f1aedfe8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,6 +9,14 @@ We'll work with you to refine your contribution, but we'll also push back if som Here are a few suggestions to make it easier for you to contribute: +### Important Developer Infos +* [Project Structure, Files and Directories](.github/copilot-instructions.md#project-structure-overview) (in our AI instructions) +* [Instructions for creating usermods](.github/copilot-instructions.md#usermod-guidelines) (in our AI instructions) +* KB: [Compiling WLED](https://kno.wled.ge/advanced/compiling-wled/) - slightly outdated but still helpful :-) +* Arduino IDE is not supported any more. Use VSCode with the PlatformIO extension. +* [Compiling in VSCode/Platformio](https://github.com/wled/WLED-Docs/issues/161) - modern way without command line or platformio.ini changes. +* If you add a new feature, consider making a PR to [``wled-docs``](https://github.com/wled/WLED-Docs) for updating our official documentation. + ### PR from a branch in your own fork Start your pull request (PR) in a branch of your own fork. Don't make a PR directly from your main branch. This lets you update your PR if needed, while you can work on other tasks in 'main' or in other branches. @@ -20,10 +28,10 @@ This lets you update your PR if needed, while you can work on other tasks in 'ma > > image: fork and edit - ### Target branch for pull requests -Please make all PRs against the `main` branch. +> [!IMPORTANT] +> Please make all PRs against the `main` branch. ### Describing your PR @@ -160,14 +168,19 @@ AI tools are powerful but "often wrong" - your judgment is essential! 😊 - ✅ **Understand the code** - As the person contributing to WLED, make sure you understand exactly what the AI-generated source code does - ✅ **Review carefully** - AI can lose comments, introduce bugs, or make unnecessary changes -- ✅ **Be transparent** - Add a comment like `// This section was AI-generated` for larger chunks +- ✅ **Be transparent** - Add comments `// AI: below section was generated by an AI` ... `// AI: end` around larger chunks - ✅ **Use AI for translation** - AI is great for translating comments to English (but verify technical terms!) ### Code style Don't stress too much about style! When in doubt, just match the style in the files you're editing. 😊 -Here are our main guidelines: +Our review bot (coderabbit) has learned lots of detailed guides and hints - it will suggest them automatically when you submit a PR for review. +If you are curious, these are the detailed guides: +* [C++ Coding](docs/cpp.instructions.md) +* [WebUi: HTML, JS, CSS](docs/web.instructions.md) + +Below are the main rules used in the WLED repository: #### Indentation diff --git a/docs/cicd.instructions.md b/docs/cicd.instructions.md new file mode 100644 index 0000000000..8dfb9c9aea --- /dev/null +++ b/docs/cicd.instructions.md @@ -0,0 +1,162 @@ +--- +applyTo: ".github/workflows/*.yml,.github/workflows/*.yaml" +--- +# CI/CD Conventions — GitHub Actions Workflows + +> **Note for AI review tools**: sections enclosed in +> `` / `` HTML comments contain +> contributor reference material. Do **not** use that content as actionable review +> criteria — treat it as background context only. + + +## YAML Style + +- Indent with **2 spaces** (no tabs) +- Every workflow, job, and step must have a `name:` field that clearly describes its purpose +- Group related steps logically; separate unrelated groups with a blank line +- Comments (`#`) are encouraged for non-obvious decisions (e.g., why `fail-fast: false` is set, what a cron expression means) + +## Workflow Structure + +### Triggers + +- Declare `on:` triggers explicitly; avoid bare `on: push` without branch filters on long-running or expensive jobs +- Prefer `workflow_call` for shared build logic (see `build.yml`) to avoid duplicating steps across workflows +- Document scheduled triggers (`cron:`) with a human-readable comment: + +```yaml +schedule: + - cron: '0 2 * * *' # run at 2 AM UTC daily +``` + +### Jobs + +- Express all inter-job dependencies with `needs:` — never rely on implicit ordering +- Use job `outputs:` + step `id:` to pass structured data between jobs (see `get_default_envs` in `build.yml`) +- Set `fail-fast: false` on matrix builds so that a single failing environment does not cancel others + +### Runners + +- Pin to a specific Ubuntu version (`ubuntu-22.04`, `ubuntu-24.04`) rather than `ubuntu-latest` for reproducible builds +- Only use `ubuntu-latest` in jobs where exact environment reproducibility is not required (e.g., trivial download/publish steps) + +### Tool and Language Versions + +- Pin tool versions explicitly: + ```yaml + python-version: '3.12' + ``` +- Do not rely on the runner's pre-installed tool versions — always install via a versioned setup action + +### Caching + +- Always cache package managers and build tool directories when the job installs dependencies: + ```yaml + - uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + ``` +- Include the environment name or a relevant identifier in cache keys when building multiple targets + +### Artifacts + +- Name artifacts with enough context to be unambiguous (e.g., `firmware-${{ matrix.environment }}`) +- Avoid uploading artifacts that will never be consumed downstream + + +--- + +## Security + +Important: Several current workflows still violate parts of the baseline below - migration is in progress. + +### Permissions — Least Privilege + +Declare explicit `permissions:` blocks. The default token permissions are broad; scope them to the minimum required: + +```yaml +permissions: + contents: read # for checkout +``` + +For jobs that publish releases or write to the repository: + +```yaml +permissions: + contents: write # create/update releases +``` + +A common safe baseline for build-only jobs: + +```yaml +permissions: + contents: read +``` + +### Supply Chain — Action Pinning + +**Third-party actions** (anything outside the `actions/` and `github/` namespaces) should be pinned to a specific release tag. Branch pins (`@main`, `@master`) are **not allowed** — they can be updated by the action author at any time without notice: + +```yaml +# ✅ Acceptable — specific version tag. SHA pinning recommended for more security, as @v2 is still a mutable tag. +uses: softprops/action-gh-release@v2 + +# ❌ Not acceptable — mutable branch reference +uses: andelf/nightly-release@main +``` + +SHA pinning (e.g., `uses: someorg/some-action@abc1234`) is the most secure option for third-party actions; it is recommended when auditing supply-chain risk is a priority. At minimum, always use a specific version tag. + +**First-party actions** (`actions/checkout`, `actions/cache`, `actions/upload-artifact`, etc.) pinned to a major version tag (e.g., `@v4`) are acceptable because GitHub maintains and audits these. + +When adding a new third-party action: +1. Check that the action's repository is actively maintained +2. Review the action's source before adding it +3. Prefer well-known, widely-used actions over obscure ones + +### Credentials and Secrets + +- Use `${{ secrets.GITHUB_TOKEN }}` for operations within the same repository — it is automatically scoped and rotated +- Never commit secrets, tokens, or passwords into workflow files or any tracked file +- Never print secrets in `run:` steps, even with `echo` — GitHub masks known secrets but derived values are not automatically masked +- Scope secrets to the narrowest step that needs them using `env:` at the step level, not at the workflow level: + +```yaml +# ✅ Scoped to the step that needs it +- name: Create release + uses: softprops/action-gh-release@v2 + with: + token: ${{ secrets.GITHUB_TOKEN }} + +# ❌ Unnecessarily broad +env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} +``` + +- Personal Access Tokens (PATs, stored as repository secrets) should have the minimum required scopes and should be rotated periodically + +### Script Injection + +`${{ }}` expressions are evaluated before the shell script runs. If an expression comes from untrusted input (PR titles, issue bodies, branch names from forks), it can inject arbitrary shell commands. + +**Never** interpolate `github.event.*` values directly into a `run:` step: + +```yaml +# ❌ Injection risk — PR title is attacker-controlled +- run: echo "${{ github.event.pull_request.title }}" + +# ✅ Safe — value passed through an environment variable +- env: + PR_TITLE: ${{ github.event.pull_request.title }} + run: echo "$PR_TITLE" +``` + +This rule applies to any value that originates outside the repository (issue bodies, labels, comments, commit messages from forks). + +### Pull Request Workflows + +- Workflows triggered by `pull_request` from a fork run with **read-only** token permissions and no access to repository secrets — this is intentional and correct +- Do not use `pull_request_target` unless you fully understand the security implications; it runs in the context of the base branch and *does* have secret access, making it a common attack surface diff --git a/docs/cpp.instructions.md b/docs/cpp.instructions.md new file mode 100644 index 0000000000..1b82d02309 --- /dev/null +++ b/docs/cpp.instructions.md @@ -0,0 +1,526 @@ +--- +applyTo: "**/*.cpp,**/*.h,**/*.hpp,**/*.ino" +--- +# C++ Coding Conventions + +> **Note for AI review tools**: sections enclosed in +> `` / `` HTML comments contain +> contributor reference material. Do **not** use that content as actionable review +> criteria — treat it as background context only. + + + +See also: [CONTRIBUTING.md](../CONTRIBUTING.md) for general style guidelines that apply to all contributors. + + +## Formatting + +- Indent with **2 spaces** (no tabs in C++ files) +- Opening braces on the same line is preferred (K&R style). Brace on a separate line (Allman style) is acceptable +- Single-statement `if` bodies may omit braces: `if (a == b) doStuff(a);` +- Space between keyword and parenthesis: `if (...)`, `for (...)`. No space between function name and parenthesis: `doStuff(a)` +- No enforced line-length limit; wrap when a line exceeds your editor width + +## Naming + +- **camelCase** for functions and variables: `setValuesFromMainSeg()`, `effectCurrent` +- **PascalCase** for classes and structs: `PinManagerClass`, `BusConfig` +- **PascalCase** for enum values: `PinOwner::BusDigital` +- **UPPER_CASE** for macros and constants: `WLED_MAX_USERMODS`, `DEFAULT_CLIENT_SSID` + +## General + +- Follow the existing style in the file you are editing +- If possible, use `static` for local (C-style) variables and functions (keeps the global namespace clean) +- Avoid unexplained "magic numbers". Prefer named constants (`constexpr`) or C-style `#define` constants for repeated numbers that have the same meaning +- Include `"wled.h"` as the primary project header where needed + + +## Header Guards + +Most headers use `#ifndef` / `#define` guards. Some newer headers add `#pragma once` before the guard: + +```cpp +#ifndef WLED_EXAMPLE_H +#define WLED_EXAMPLE_H +// ... +#endif // WLED_EXAMPLE_H +``` + + +## Comments + +- `//` for inline comments, `/* ... */` for block comments. Always put a space after `//` +- **AI attribution:** When a larger block of code is generated by an AI tool, mark it with an `// AI:` comment so reviewers know to scrutinize it: + +```cpp +// AI: below section was generated by an AI +void calculateCRC(const uint8_t* data, size_t len) { + ... +} +// AI: end +``` + + Single-line AI-assisted edits do not need the marker — use it when the AI produced a contiguous block that a human did not write line-by-line. + + + +- **Function & feature comments:** Every non-trivial function should have a brief comment above it describing what it does. Include a note about each parameter when the names alone are not self-explanatory: + +```cpp +/* ***** + * Apply gamma correction to a single color channel. + * @param value raw 8-bit channel value (0–255) + * @param gamma gamma exponent (typically 2.8) + * @return corrected 8-bit value + ***** */ +uint8_t gammaCorrect(uint8_t value, float gamma); +``` + + + Short accessor-style functions (getters/setters, one-liners) may skip this if their purpose is obvious from the name. + +## Preprocessor & Feature Flags + +- Prefer compile-time feature flags (`#ifdef` / `#ifndef`) over runtime checks where possible +- Platform differentiation: `ARDUINO_ARCH_ESP32` vs `ESP8266` +- PSRAM availability: `BOARD_HAS_PSRAM` + +## Error Handling + +- `DEBUG_PRINTF()` / `DEBUG_PRINTLN()` for developer diagnostics (compiled out unless `-D WLED_DEBUG`) +- Don't rely on C++ exceptions — use return codes (`-1` / `false` for errors) and global flags (e.g. `errorFlag = ERR_LOW_MEM`). Some builds don't support C++ exceptions. + +## Strings + +- Use `const char*` for temporary/parsed strings +- Avoid `String` (Arduino heap-allocated string) in hot paths; acceptable in config/setup code +- Use `F("string")` for string constants (major RAM win on ESP8266; mostly overload/type compatibility on ESP32) +- Store repeated strings as `static const char[] PROGMEM` + + + On **ESP8266** this explicitly stores the string in flash (PROGMEM), saving precious RAM — every byte counts on that platform. + On **ESP32**, `PROGMEM` is a no-op and string literals already reside in flash/rodata, so `F()` yields little RAM benefit but remains harmless (it satisfies `__FlashStringHelper*` overloads that some APIs expect). + + +```cpp + DEBUG_PRINTLN(F("WS client connected.")); // string stays in flash, not RAM + DEBUG_PRINTF_P(PSTR("initE: Ignoring attempt for invalid ethernetType (%d)\n"), ethernetType); // format string stays in flash +``` + +## Memory + +- **PSRAM-aware allocation**: use `d_malloc()` (prefer DRAM), `p_malloc()` (prefer PSRAM) from `fcn_declare.h` +- **Avoid Variable Length Arrays (VLAs)**: FreeRTOS task stacks are typically 2–8 KB. A runtime-sized VLA can silently exhaust the stack. Use fixed-size arrays or heap allocation (`d_malloc` / `p_malloc`). Any VLA must be explicitly justified in source or PR. + +GCC/Clang support VLAs as an extension (they are not part of the C++ standard), so they look like a legitimate feature — but they are allocated on the stack at runtime. On ESP32/ESP8266, a VLA whose size depends on a runtime parameter (segment dimensions, pixel counts, etc.) can silently exhaust the stack and cause the program to behave in unexpected ways or crash. + +- **Larger buffers** (LED data, JSON documents) should use PSRAM when available and technically feasible +- **Hot-path**: some data should stay in DRAM or IRAM for performance reasons +- Memory efficiency matters, but is less critical on boards with PSRAM + +Heap fragmentation is a concern: + + - Fragmentation can lead to crashes, even when the overall amount of available heap is still good. The C++ runtime doesn't do any "garbage collection". + + - Avoid frequent `d_malloc` and `d_free` inside a function, especially for small sizes. + - Avoid frequent creation / destruction of objects. + - Allocate buffers early, and try to re-use them. + - Instead of incrementally appending to a `String`, reserve the expected max buffer upfront by using the `reserve()` method. + + +```cpp + String result; + result.reserve(65); // pre-allocate to avoid realloc fragmentation +``` + +```cpp + // prefer DRAM; falls back gracefully and enforces MIN_HEAP_SIZE guard + _ledsDirty = (byte*) d_malloc(getBitArrayBytes(_len)); +``` + +```cpp + _mode.reserve(_modeCount); // allocate memory to prevent initial fragmentation - does not increase size() + _modeData.reserve(_modeCount); // allocate memory to prevent initial fragmentation - does not increase size() +``` + + +## `const` and `constexpr` + +`const` is a promise to the compiler that a value (or object) will not change - a function declared with a `const char* message` parameter is not allowed to modify the content of `message`. +This pattern enables optimizations and makes intent clear to reviewers. + +`constexpr` allows to define constants that are *guaranteed* to be evaluated by the compiler (zero run-time costs). + + +- For function parameters that are read-only, prefer `const &` or `const`. + +### `const` locals + +* Adding `const` to a local variable that is only assigned once is optional, but *not* strictly necessary. + +* In hot-path code, `const` on cached locals may help the compiler keep values in registers. + ```cpp + const uint_fast16_t cols = vWidth(); + const uint_fast16_t rows = vHeight(); + ``` + +### `const` references to avoid copies +- Pass objects by `const &` (or `&`) instead of copying them implicitly. +- Use `const &` (or `&`) inside loops - This avoids constructing temporary objects on every access. + + +```cpp +const auto &m = _mappings[i]; // reference, not a copy (bus_manager.cpp) +Segment& sourcesegment = strip.getSegment(sourceid); // alias — avoids creating a temporary Segment instance +``` + +For function parameters that are read-only, prefer `const &`: +```cpp +BusManager::add(const BusConfig &bc, bool placeholder) { +``` + + +- Class **Data Members:** Avoid reference data members (`T&` or `const T&`) in a class. + A reference member can outlive the object it refers to, causing **dangling reference** bugs that are hard to diagnose. Prefer value storage or use a pointer and document the expected lifetime. + + + +### `constexpr` over `#define` + +- Prefer `constexpr` for compile-time constants. Unlike `#define`, `constexpr` respects scope and type safety, keeping the global namespace clean. + +```cpp +// Prefer: +constexpr uint32_t TWO_CHANNEL_MASK = 0x00FF00FF; +constexpr int WLED_MAX_BUSSES = WLED_MAX_DIGITAL_CHANNELS + WLED_MAX_ANALOG_CHANNELS; + +// Avoid (when possible): +#define TWO_CHANNEL_MASK 0x00FF00FF +``` + + +### `static_assert` over `#error` + +- Use `static_assert` instead of the C-style `#if … #error … #endif` pattern when validating compile-time constants. It provides a clear message and works with `constexpr` values. +- `#define` and `#if ... #else ... #endif` is still needed for conditional-compilation guards and build-flag-overridable values. + + +```cpp +// Prefer: +constexpr int WLED_MAX_BUSSES = WLED_MAX_DIGITAL_CHANNELS + WLED_MAX_ANALOG_CHANNELS; +static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit"); + +// Avoid: +#if (WLED_MAX_BUSSES > 32) + #error "WLED_MAX_BUSSES exceeds hard limit" +#endif +``` + +```cpp + // using static_assert() to validate enumerated types (zero cost at runtime) + static_assert(0u == static_cast(PinOwner::None), + "PinOwner::None must be zero, so default array initialization works as expected"); +``` + + +### `static` and `const` class methods + +#### `const` member functions + +Marking a member function `const` tells the compiler that it does not modify the object's state: + +```cpp +uint16_t length() const { return _len; } +bool isActive() const { return _active; } +``` + +Benefits for GCC/Xtensa/RISC-V: +- The compiler knows the method cannot write to `this`, so it is free to **keep member values in registers** across the call and avoid reload barriers. +- `const` methods can be called on `const` objects and `const` references — essential when passing large objects as `const &` to avoid copying. +- `const` allows the compiler to **eliminate redundant loads**: if a caller already has a member value cached, the compiler can prove the `const` call cannot invalidate it. + + +Declare getter, query, or inspection methods `const`. If you need to mark a member `mutable` to work around this (e.g. for a cache or counter), document the reason. + +#### `static` member functions + + +A `static` member function has no implicit `this` pointer. This has two distinct advantages: + +1. **Smaller code, faster calls**: no `this` is passed in a register. On Xtensa and RISC-V, this removes one register argument from every call site and prevents the compiler from emitting `this`-preservation code around inlined blocks. +2. **Better inlining**: GCC can inline a `static` method with more certainty because it cannot be overridden by a derived class (no virtual dispatch ambiguity) and has no aliasing concern through `this`. + +Use `static` for any method that does not need access to instance members: + +```cpp +// Factory / utility — no instance needed: +static BusConfig fromJson(JsonObject obj); + +// Pure computation helpers: +static uint8_t gamma8(uint8_t val); +static uint32_t colorBalance(uint32_t color, uint8_t r, uint8_t g, uint8_t b); +``` + + +`static` communicates intent clearly: a reviewer immediately knows the method is stateless and safe to call without a fully constructed object. + +> **Rule of thumb**: if a method does not read or write any member variable, make it `static`. If it only reads member variables, make it `const`. Note: `static` methods cannot also be `const`-qualified because there is no implicit `this` pointer to be const — just use `static`. Both qualifiers reduce coupling and improve generated code on all ESP32 targets. + +--- + +## Hot-Path Optimization + +The hot path is the per-frame pixel pipeline: **Segment → Strip → BusManager → Bus(Digital,HUB75,Network) or PolyBus → LED driver, plus ``WS2812FX::show()`` and below**. +Speed is the priority here. The patterns below are taken from existing hot-path code (`FX_fcn.cpp`, `FX_2Dfcn.cpp`, `bus_manager.cpp`, `colors.cpp`) and should be followed when modifying these files. + +Note: `FX.cpp` (effect functions) is written by many contributors and has diverse styles — that is acceptable. + +### Function Attributes + +Stack the appropriate attributes on hot-path functions. Defined in `const.h`: + +| Attribute | Meaning | When to use | +|---|---|---| +| `__attribute__((hot))` | Branch-prediction hint | hot-path functions with complex logic | +| `IRAM_ATTR` | Place in fast IRAM (ESP32) | Critical per-pixel functions (e.g. `BusDigital::setPixelColor`) | +| `IRAM_ATTR_YN` | IRAM on ESP32, no-op on ESP8266 | Hot functions that ESP8266 can't fit in IRAM | +| `WLED_O2_ATTR` | Force `-O2` optimization | Most hot-path functions | +| `WLED_O3_ATTR` | Force `-O3,fast-math` | Innermost color math (e.g. `color_blend`) | +| `[[gnu::hot]] inline` | Modern C++ attribute + inline | Header-defined accessors (e.g. `progress()`, `currentBri()`) | + +Note: `WLED_O3_ATTR` sometimes causes performance loss compared to `WLED_O2_ATTR`. Choose optimization levels based on test results. + +Example signature: + +```cpp +void IRAM_ATTR_YN WLED_O2_ATTR __attribute__((hot)) Segment::setPixelColor(unsigned i, uint32_t c) +``` + + +### Cache Members to Locals Before Loops + +Copy class members and virtual-call results to local variables before entering a loop: + +```cpp +uint_fast8_t count = numBusses; // avoid repeated member access +for (uint_fast8_t i = 0; i < count; i++) { + Bus* const b = busses[i]; // const pointer hints to compiler + uint_fast16_t bstart = b->getStart(); + uint_fast16_t blen = b->getLength(); + ... +} +``` + + +### Unsigned Range Check + +Replace two-comparison range tests with a single unsigned subtraction: + +```cpp +// Instead of: if (pix >= bstart && pix < bstart + blen) +if ((uint_fast16_t)(pix - bstart) < blen) // also catches negative pix via unsigned underflow +``` + +### Early Returns + +Guard every hot-path function with the cheapest necessary checks first: + +```cpp +if (!isActive()) return; // inactive segment +if (unsigned(i) >= vLength()) return; // bounds check (catches negative i too) +``` + +### Avoid Nested Calls — Fast Path / Complex Path + +Avoid calling non-inline functions or making complex decisions inside per-pixel hot-path code. When a function has both a common simple case and a rare complex case, split it into two variants and choose once per frame rather than per pixel. + +General rules: +- Keep fast-path functions free of non-inline calls, multi-way branches, and complex switch-case decisions. +- Hoist per-frame decisions (e.g. simple vs. complex segment) out of the per-pixel loop. +- Code duplication between fast/slow variants is acceptable to keep the fast path lean. + +### Function Pointers to Eliminate Repeated Decisions + +When the same decision (e.g. "which drawing routine?") would be evaluated for every pixel, assign the chosen variant to a function pointer once and let the inner loop call through the pointer. This removes the branch entirely — the calling code (e.g. the GIF decoder loop) only ever invokes one function per frame, with no per-pixel decision. + + +`image_loader.cpp` demonstrates the pattern: `calculateScaling()` picks the best drawing callback once per frame based on segment dimensions and GIF size, then passes it to the decoder via `setDrawPixelCallback()`: + +```cpp +// calculateScaling() — called once per frame +if ((perPixelX < 2) && (perPixelY < 2)) + decoder.setDrawPixelCallback(drawPixelCallbackDownScale2D); // downscale-only variant +else + decoder.setDrawPixelCallback(drawPixelCallback2D); // full-scaling variant +``` + +Each callback is a small, single-purpose function with no internal branching — the decoder's per-pixel loop never re-evaluates which strategy to use. + + +### Template Specialization (Advanced) + +Templates can eliminate runtime decisions by generating separate code paths at compile time. For example, a pixel setter could be templated on color order or channel count so the compiler removes dead branches and produces tight, specialized machine code: + +```cpp +template +void setChannel(uint8_t* out, uint32_t col) { + out[0] = R(col); out[1] = G(col); out[2] = B(col); + if constexpr (hasWhite) out[3] = W(col); // compiled out when hasWhite==false +} +``` + +Use sparingly — each instantiation duplicates code in flash. On ESP8266 and small-flash ESP32 boards this can exhaust IRAM/flash. Prefer templates only when the hot path is measurably faster and the number of instantiations is small (2–4). + +### RAII Lock-Free Synchronization (Advanced) + +Where contention is rare and the critical section is short, consider replacing mutex-based locking with lock-free techniques using `std::atomic` and RAII scoped guards. A scoped guard sets a flag on construction and clears it on destruction, guaranteeing cleanup even on early return: + +```cpp +struct ScopedBusyFlag { + std::atomic& flag; + bool acquired; + ScopedBusyFlag(std::atomic& f) : flag(f), acquired(false) { + bool expected = false; + acquired = flag.compare_exchange_strong(expected, true); + } + ~ScopedBusyFlag() { if (acquired) flag.store(false); } + explicit operator bool() const { return acquired; } +}; + +// Usage +static std::atomic busySending{false}; +ScopedBusyFlag guard(busySending); +if (!guard) return; // another task is already sending +// ... do work — flag auto-clears when guard goes out of scope +``` + +This avoids FreeRTOS semaphore overhead and the risk of forgetting to return a semaphore. There are no current examples of this pattern in the codebase — consult with maintainers before introducing it in new code, to ensure it aligns with the project's synchronization conventions. + + +### Pre-Compute Outside Loops + +Move invariant calculations before the loop. Pre-compute reciprocals to replace division with multiplication. + +```cpp +const uint_fast16_t cols = virtualWidth(); +const uint_fast16_t rows = virtualHeight(); +uint_fast8_t fadeRate = (255U - rate) >> 1; +float mappedRate_r = 1.0f / (float(fadeRate) + 1.1f); // reciprocal — avoid division inside loop +``` + + +### Parallel Channel Processing + +Process R+B and W+G channels simultaneously using the two-channel mask pattern: + +```cpp +constexpr uint32_t TWO_CHANNEL_MASK = 0x00FF00FF; +uint32_t rb = (((c1 & TWO_CHANNEL_MASK) * amount) >> 8) & TWO_CHANNEL_MASK; +uint32_t wg = (((c1 >> 8) & TWO_CHANNEL_MASK) * amount) & ~TWO_CHANNEL_MASK; +return rb | wg; +``` + +### Bit Shifts Over Division (mainly for RISC-V boards) + +ESP32 and ESP32-S3 (Xtensa core) have a fast "integer divide" instruction, so manual shifts rarely help. +On RISC-V targets (ESP32-C3/C6/P4) and ESP8266, prefer explicit bit-shifts for power-of-two arithmetic — the compiler does **not** always convert divisions to shifts. +Always use unsigned operands for right shifts; signed right-shift is implementation-defined. + + +On RISC-V-based boards (ESP32-C3, ESP32-C6, ESP32-C5) explicit shifts can be beneficial. +```cpp +position >> 3 // instead of position / 8 +(255U - rate) >> 1 // instead of (255 - rate) / 2 +i & 0x0007 // instead of i % 8 +``` + +**Important**: The bit-shifted expression should be unsigned. On some MCUs, "signed right-shift" is implemented by an "arithmetic shift right" that duplicates the sign bit: ``0b1010 >> 1 = 0b1101``. + + +### Static Caching for Expensive Computations + +Cache results in static locals when the input rarely changes between calls: + +```cpp +static uint16_t lastKelvin = 0; +static byte correctionRGB[4] = {255,255,255,0}; +if (lastKelvin != kelvin) { + colorKtoRGB(kelvin, correctionRGB); // expensive — only recalculate when input changes + lastKelvin = kelvin; +} +``` + +### Inlining Strategy + +- Move frequently-called small functions to headers for inlining (e.g. `Segment::setPixelColorRaw` is in `FX.h`) +- Use `static inline` for file-local helpers + +### Math & Trigonometric Functions + +- WLED uses a custom `fastled_slim` library. The old FastLED trig aliases (`sin8`, `cos8`, `sin16`, `cos16`) **no longer exist and cause a compile error** — use `sin8_t()`, `cos8_t()`, `sin16_t()`, `cos16_t()` instead. For float approximations use `sin_approx()` / `cos_approx()` instead of `sinf()` / `cosf()`. Replace FastLED noise aliases (`inoise8`, `inoise16`) with `perlin8`, `perlin16`. + + +| ❌ Do not use (compile error) | ✅ Use instead | Source | +|---|---|---| +| `sin8()`, `cos8()` | `sin8_t()`, `cos8_t()` | `fastled_slim.h` → `wled_math.cpp` | +| `sin16()`, `cos16()` | `sin16_t()`, `cos16_t()` | `fastled_slim.h` → `wled_math.cpp` | +| `sinf()`, `cosf()` | `sin_approx()`, `cos_approx()` | `wled_math.cpp` | +| `atan2f()`, `atan2()` | `atan2_t()` | `wled_math.cpp` | +| `sqrt()` on integers | `sqrt32_bw()` | `fcn_declare.h` → `wled_math.cpp` | +| `sqrtf()` on floats | `sqrtf()` (acceptable) | — no WLED replacement | + + +--- + +## `delay()` vs `yield()` in ESP32 Tasks + +* On ESP32, `delay(ms)` calls `vTaskDelay(ms / portTICK_PERIOD_MS)`, which **suspends only the calling task**. The FreeRTOS scheduler immediately runs all other ready tasks. +* The Arduino `loop()` function runs inside `loopTask`. Calling `delay()` there does *not* block the network stack, audio FFT, LED DMA, nor any other FreeRTOS task. +* This differs from ESP8266, where `delay()` stalls the entire system unless `yield()` was called inside. + + +- On ESP32, `delay()` is generally allowed, as it helps to efficiently manage CPU usage of all tasks. +- On ESP8266, only use `delay()` and `yield()` in the main `loop()` context. If not sure, protect with `if (can_yield()) ...`. +- Do *not* use `delay()` in effects (FX.cpp) or in the hot pixel path. +- `delay()` on the bus-level is allowed, it might be needed to achieve exact timing in LED drivers. + +### IDLE Watchdog and Custom Tasks on ESP32 + +- In arduino-esp32, `yield()` calls `vTaskDelay(0)`, which only switches to tasks at equal or higher priority — the IDLE task (priority 0) is never reached. +- **Do not use `yield()` to pace ESP32 tasks or assume it feeds any watchdog**. +- **Custom `xTaskCreate()` tasks must call `delay(1)` in their loop, not `yield()`.** Without a real blocking call, the IDLE task is starved. The IDLE watchdog panic is the first visible symptom — but the damage starts earlier: deleted task memory leaks, software timers stop firing, light sleep is disabled, and Wi-Fi/BT idle hooks don't run. Structure custom tasks like this: +```cpp +// WRONG — IDLE task is never scheduled; yield() does not feed the idle task watchdog. +void myTask(void*) { + for (;;) { + doWork(); + yield(); + } +} + +// CORRECT — delay(1) suspends the task for ≥1 ms, IDLE task runs, IDLE watchdog is fed +void myTask(void*) { + for (;;) { + doWork(); + delay(1); // DO NOT REMOVE — lets IDLE(0) run and feeds its watchdog + } +} +``` + +- Prefer blocking FreeRTOS primitives (`xQueueReceive`, `ulTaskNotifyTake`, `vTaskDelayUntil`) over `delay(1)` polling where precise timing or event-driven behaviour is needed. +- **Watchdog note.** WLED disables the Task Watchdog by default (`WLED_WATCHDOG_TIMEOUT 0` in `wled.h`). When enabled, `esp_task_wdt_reset()` is called at the end of each `loop()` iteration. Long blocking operations inside `loop()` — such as OTA downloads or slow file I/O — must call `esp_task_wdt_reset()` periodically, or be restructured so the main loop is not blocked for longer than the configured timeout. + +## Caveats and Pitfalls + +- **LittleFS filenames**: File paths passed to `file.open()` must not exceed 255 bytes (`LFS_NAME_MAX`). Validate constructed paths (e.g., `/ledmap_` + segment name + `.json`) stay within this limit (assume standard configurations, like WLED_MAX_SEGNAME_LEN = 64). + +- **Float-to-unsigned conversion is undefined behavior when the value is out of range.** Converting a negative `float` directly to an unsigned integer type (`uint8_t`, `uint16_t`, …) is UB per the C++ standard — the Xtensa (ESP32) toolchain may silently wrap, but RISC-V (ESP32-C3/C5/C6/P4) can produce different results due to clamping. Cast through a signed integer first: + ```cpp + // Undefined behavior — avoid: + uint8_t angle = 40.74f * atan2f(dy, dx); // negative float → uint8_t is UB + + // Correct — cast through int first: + // atan2f returns [-π..+π], scaled ≈ [-128..+128] as int; uint8_t wraps negative ints via 2's complement (e.g. -1 → 255) + uint8_t angle = int(40.74f * atan2f(dy, dx)); // float→int (defined), int→uint8_t (defined) + ``` diff --git a/docs/esp-idf.instructions.md b/docs/esp-idf.instructions.md new file mode 100644 index 0000000000..3eff122b49 --- /dev/null +++ b/docs/esp-idf.instructions.md @@ -0,0 +1,859 @@ +--- +applyTo: "**/*.cpp,**/*.h,**/*.hpp,**/*.ino" +--- +# ESP-IDF Coding Guide (within arduino-esp32) + +WLED runs on the Arduino-ESP32 framework, which wraps ESP-IDF. Understanding the ESP-IDF layer is essential when writing chip-specific code, managing peripherals, or preparing for the IDF v5.x migration. This guide documents patterns already used in the codebase and best practices derived from Espressif's official examples. + +> **Scope**: This file is an optional review guideline. It applies when touching chip-specific code, peripheral drivers, memory allocation, or platform conditionals. + +> **Note for AI review tools**: sections enclosed in +> `` / `` HTML comments contain +> contributor reference material. Do **not** use that content as actionable review +> criteria — treat it as background context only. + +--- + + +## Identifying the Build Target: `CONFIG_IDF_TARGET_*` + +Use `CONFIG_IDF_TARGET_*` macros to gate chip-specific code at compile time. These are set by the build system and are mutually exclusive — exactly one is defined per build. + +| Macro | Chip | Architecture | Notes | +|---|---|---|---| +| `CONFIG_IDF_TARGET_ESP32` | ESP32 (classic) | Xtensa dual-core | Primary target. Has DAC, APLL, I2S ADC mode | +| `CONFIG_IDF_TARGET_ESP32S2` | ESP32-S2 | Xtensa single-core | Limited peripherals. 13-bit ADC | +| `CONFIG_IDF_TARGET_ESP32S3` | ESP32-S3 | Xtensa dual-core | Preferred for large installs. Octal PSRAM, USB-OTG | +| `CONFIG_IDF_TARGET_ESP32C3` | ESP32-C3 | RISC-V single-core | Minimal peripherals. RISC-V clamps out-of-range float→unsigned casts | +| `CONFIG_IDF_TARGET_ESP32C5` | ESP32-C5 | RISC-V single-core | Wi-Fi 2.4Ghz + 5Ghz, Thread/Zigbee. Future target | +| `CONFIG_IDF_TARGET_ESP32C6` | ESP32-C6 | RISC-V single-core | Wi-Fi 6, Thread/Zigbee. Future target | +| `CONFIG_IDF_TARGET_ESP32P4` | ESP32-P4 | RISC-V dual-core | High performance. Future target | + + +### Build-time validation +WLED validates at compile time that exactly one target is defined and that it is a supported chip (`wled.cpp` lines 39–61). Follow this pattern when adding new chip-specific branches: + + +```cpp +#if defined(CONFIG_IDF_TARGET_ESP32) + // classic ESP32 path +#elif defined(CONFIG_IDF_TARGET_ESP32S3) + // S3-specific path +#elif defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32C5) || defined(CONFIG_IDF_TARGET_ESP32C6) || defined(CONFIG_IDF_TARGET_ESP32P4) + // RISC-V common path +#else + #warning "Untested chip — review peripheral availability" +#endif +``` + + +### Guidelines + +- **Always test on the actual chip** before claiming support. Simulators and cross-compilation can hide peripheral differences. +- **Prefer `#elif` chains** over nested `#ifdef` for readability. +- **Do not use `CONFIG_IDF_TARGET_*` for feature detection.** Use `SOC_*` capability macros instead (see next section). For example, use `SOC_I2S_SUPPORTS_ADC` instead of `CONFIG_IDF_TARGET_ESP32` to check for I2S ADC support. +- When a feature must be disabled on certain chips, use explicit `static_assert()` or `#warning` directives so the build clearly reports what is missing. + +--- + +## Hardware Capability Detection: `SOC_*` Macros + +`SOC_*` macros (from `soc/soc_caps.h`) describe what the current chip supports. They are the correct way to check for peripheral features — they stay accurate when new chips are added, unlike `CONFIG_IDF_TARGET_*` checks. + + +### Important `SOC_*` macros used in WLED + +| Macro | Type | Used in | Purpose | +|---|---|---|---| +| `SOC_I2S_NUM` | `int` | `audio_source.h` | Number of I2S peripherals (1 or 2) | +| `SOC_I2S_SUPPORTS_ADC` | `bool` | `usermods/audioreactive/audio_source.h` | I2S ADC sampling mode (ESP32 only) | +| `SOC_I2S_SUPPORTS_APLL` | `bool` | `usermods/audioreactive/audio_source.h` | Audio PLL for precise sample rates | +| `SOC_I2S_SUPPORTS_PDM_RX` | `bool` | `usermods/audioreactive/audio_source.h` | PDM microphone input | +| `SOC_ADC_MAX_BITWIDTH` | `int` | `util.cpp` | ADC resolution (12 or 13 bits). Renamed to `CONFIG_SOC_ADC_RTC_MAX_BITWIDTH` in IDF v5 | +| `SOC_ADC_CHANNEL_NUM(unit)` | `int` | `pin_manager.cpp` | ADC channels per unit | +| `SOC_UART_NUM` | `int` | `dmx_input.cpp` | Number of UART peripherals | +| `SOC_DRAM_LOW` / `SOC_DRAM_HIGH` | `addr` | `util.cpp` | DRAM address boundaries for validation | + + +### Key pitfall +`SOC_ADC_MAX_BITWIDTH` (ADC resolution 12 or 13 bits) was renamed to `CONFIG_SOC_ADC_RTC_MAX_BITWIDTH` in IDF v5. + + +### Less commonly used but valuable + +| Macro | Purpose | +|---|---| +| `SOC_RMT_TX_CANDIDATES_PER_GROUP` | Number of RMT TX channels (varies 2–8 by chip) | +| `SOC_LEDC_CHANNEL_NUM` | Number of LEDC (PWM) channels | +| `SOC_GPIO_PIN_COUNT` | Total GPIO pin count | +| `SOC_DAC_SUPPORTED` | Whether the chip has a DAC (ESP32/S2 only) | +| `SOC_SPIRAM_SUPPORTED` | Whether PSRAM interface exists | +| `SOC_CPU_CORES_NUM` | Core count (1 or 2) — useful for task pinning decisions | + + + +### Best practices + +```cpp +// Good: feature-based detection +#if SOC_I2S_SUPPORTS_PDM_RX + _config.mode = i2s_mode_t(I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_PDM); +#else + #warning "PDM microphones not supported on this chip" +#endif + +// Avoid: chip-name-based detection +#if defined(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32S3) + // happens to be correct today, but breaks when a new chip adds PDM support +#endif +``` + + +### PSRAM capability macros + +For PSRAM presence, mode, and DMA access patterns: + +| Macro | Meaning | +|---|---| +| `CONFIG_SPIRAM` / `BOARD_HAS_PSRAM` | PSRAM is present in the build configuration | +| `CONFIG_SPIRAM_MODE_QUAD` | Quad-SPI PSRAM (standard, used on ESP32 classic and some S2/S3 boards) | +| `CONFIG_SPIRAM_MODE_OCT` | Octal-SPI PSRAM — 8 data lines, DTR mode. Used on ESP32-S3 with octal PSRAM (e.g. N8R8 / N16R8 modules). Reserves GPIO 33–37 for the PSRAM bus — **do not allocate these pins** when this macro is defined. `wled.cpp` uses this to gate GPIO reservation. | +| `CONFIG_SPIRAM_MODE_HEX` | Hex-SPI (16-line) PSRAM — future interface on ESP32-P4 running at up to 200 MHz. Used in `json.cpp` to report the PSRAM mode. | +| `CONFIG_SOC_PSRAM_DMA_CAPABLE` | PSRAM buffers can be used with DMA (ESP32-S3 with octal PSRAM) | +| `CONFIG_SOC_MEMSPI_FLASH_PSRAM_INDEPENDENT` | SPI flash and PSRAM on separate buses (no speed contention) | + + + +#### Detecting octal/hex flash + +On ESP32-S3 modules with OPI flash (e.g. N8R8 modules where the SPI flash itself runs in Octal-PI mode), the build system sets: + +| Macro | Meaning | +|---|---| +| `CONFIG_ESPTOOLPY_FLASHMODE_OPI` | Octal-PI flash mode. On S3, implies GPIO 33–37 are used by the flash/PSRAM interface — the same GPIO block as octal PSRAM. `wled.cpp` uses `CONFIG_ESPTOOLPY_FLASHMODE_OPI \|\| (CONFIG_SPIRAM_MODE_OCT && BOARD_HAS_PSRAM)` to decide whether to reserve these GPIOs. `json.cpp` uses this to report the flash mode string as `"🚀OPI"`. | +| `CONFIG_ESPTOOLPY_FLASHMODE_HEX` | Hex flash mode (ESP32-P4). Reported as `"🚀🚀HEX"` in `json.cpp`. | + +**Pattern used in WLED** (from `wled.cpp`) to reserve the octal-bus GPIOs on S3: +```cpp +#if defined(CONFIG_IDF_TARGET_ESP32S3) + #if CONFIG_ESPTOOLPY_FLASHMODE_OPI || (CONFIG_SPIRAM_MODE_OCT && defined(BOARD_HAS_PSRAM)) + // S3: GPIO 33-37 are used by the octal PSRAM/flash bus + managed_pin_type pins[] = { {33, true}, {34, true}, {35, true}, {36, true}, {37, true} }; + pinManager.allocateMultiplePins(pins, sizeof(pins)/sizeof(managed_pin_type), PinOwner::SPI_RAM); + #endif +#endif +``` + +--- + +## ESP-IDF Version Conditionals + + +### Checking the IDF version + +```cpp +#include + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) + // IDF v5+ code path +#elif ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0) + // IDF v4.4+ code path +#else + // Legacy IDF v3/v4.x path +#endif +``` + +### Key ESP-IDF version thresholds for WLED + +| Version | What changed | +|---|---| +| **4.0.0** | Filesystem API (`SPIFFS`/`LittleFS`), GPIO driver overhaul | +| **4.2.0** | ADC/GPIO API updates; `esp_adc_cal` introduced | +| **4.4.0** | I2S driver refactored (legacy API remains); `adc_deprecated.h` headers appear for newer targets | +| **4.4.4–4.4.8** | Known I2S channel-swap regression on ESP32 (workaround in `audio_source.h`) | +| **5.0.0** | **Major breaking changes** — RMT, I2S, ADC, SPI flash APIs replaced (see migration section) | +| **5.1.0** | Matter protocol support; new `esp_flash` API stable | +| **5.3+** | arduino-esp32 v3.x compatibility; C6/P4 support | + + +### Guidelines + +- When adding a version guard, **always include a comment** explaining *what* changed and *why* the guard is needed. +- Avoid version ranges that silently break — prefer `>=` over exact version matches. +- Known regressions should use explicit range guards: + ```cpp + // IDF 4.4.4–4.4.8 swapped I2S left/right channels (fixed in 4.4.9) + #if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 4)) && \ + (ESP_IDF_VERSION <= ESP_IDF_VERSION_VAL(4, 4, 8)) + #define I2S_CHANNELS_SWAPPED + #endif + ``` + +--- + +## Migrating from ESP-IDF v4.4.x to v5.x + +The jump from IDF v4.4 (arduino-esp32 v2.x) to IDF v5.x (arduino-esp32 v3.x) is the largest API break in ESP-IDF history. This section documents the critical changes and recommended migration patterns based on the upstream WLED `V5-C6` branch (`https://github.com/wled/WLED/tree/V5-C6`). Note: WLED has not yet migrated to IDF v5 — these patterns prepare for the future migration. + + +### Compiler changes + +IDF v5.x ships a much newer GCC toolchain. Key versions: + +| ESP-IDF | GCC | C++ default | Notes | +|---|---|---|---| +| 4.4.x (current) | **8.4.0** | C++17 (gnu++17) | Xtensa + RISC-V | +| 5.1–5.3 | **13.2** | C++20 (gnu++2b) | Significant warning changes | +| 5.4–5.5 | **14.2** | C++23 (gnu++2b) | Latest; stricter diagnostics | + +Notable behavioral differences: + +| Change | Impact | Action | +|---|---|---| +| Stricter `-Werror=enum-conversion` | Implicit int-to-enum casts now error | Use explicit `static_cast<>` or typed enums | +| C++20/23 features available | `consteval`, `concepts`, `std::span`, `std::expected` | Use judiciously — ESP8266 builds still require GCC 10.x with C++17 | +| `-Wdeprecated-declarations` enforced | Deprecated API calls become warnings/errors | Migrate to new APIs (see below) | +| `-Wdangling-reference` (GCC 13+) | Warns when a reference binds to a temporary that will be destroyed | Fix the lifetime issue; do not suppress the warning | +| `-fno-common` default (GCC 12+) | Duplicate tentative definitions across translation units cause linker errors | Use `extern` declarations in headers, define in exactly one `.cpp` | +| RISC-V codegen improvements | C3/C6/P4 benefit from better register allocation | No action needed — automatic | + +### C++ language features: GCC 8 → GCC 14 + +The jump from GCC 8.4 to GCC 14.2 spans six major compiler releases. This section lists features that become available and patterns that need updating. + +#### Features safe to use after migration + +These work in GCC 13+/14+ but **not** in GCC 8.4. Guard with `#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)` if the code must compile on both IDF v4 and v5. + +| Feature | Standard | Example | Benefit | +|---|---|---|---| +| Designated initializers (C++20) | C++20 | `gpio_config_t cfg = { .mode = GPIO_MODE_OUTPUT };` | Already used as a GNU extension in GCC 8; becomes standard and portable in C++20 | +| `[[likely]]` / `[[unlikely]]` | C++20 | `if (err != ESP_OK) [[unlikely]] { ... }` | Hints for branch prediction; useful in hot paths | +| `[[nodiscard("reason")]]` | C++20 | `[[nodiscard("leak if ignored")]] void* allocBuffer();` | Enforces checking return values — helpful for `esp_err_t` wrappers | +| `std::span` | C++20 | `void process(std::span buf)` | Safe, non-owning view of contiguous memory — replaces raw pointer + length pairs | +| `consteval` | C++20 | `consteval uint32_t packColor(...)` | Guarantees compile-time evaluation; useful for color constants | +| `constinit` | C++20 | `constinit static int counter = 0;` | Prevents static initialization order fiasco | +| Concepts / `requires` | C++20 | `template requires std::integral` | Clearer constraints than SFINAE; improves error messages | +| Three-way comparison (`<=>`) | C++20 | `auto operator<=>(const Version&) const = default;` | Less boilerplate for comparable types | +| `std::bit_cast` | C++20 | `float f = std::bit_cast(uint32_val);` | Type-safe reinterpretation — replaces `memcpy` or `union` tricks | +| `if consteval` | C++23 | `if consteval { /* compile-time */ } else { /* runtime */ }` | Cleaner than `std::is_constant_evaluated()` | +| `std::expected` | C++23 | `std::expected readSensor()` | Monadic error handling — cleaner than returning error codes | +| `std::to_underlying` | C++23 | `auto val = std::to_underlying(myEnum);` | Replaces `static_cast(myEnum)` | + +#### Features already available in GCC 8 (C++17) + +These work on both IDF v4.4 and v5.x — prefer them now: + +| Feature | Example | Notes | +|---|---|---| +| `if constexpr` | `if constexpr (sizeof(T) == 4) { ... }` | Compile-time branching; already used in WLED | +| `std::optional` | `std::optional pin;` | Nullable value without sentinel values like `-1` | +| `std::string_view` | `void log(std::string_view msg)` | Non-owning, non-allocating string reference | +| Structured bindings | `auto [err, value] = readSensor();` | Useful with `std::pair` / `std::tuple` returns | +| Fold expressions | `(addSegment(args), ...);` | Variadic template expansion | +| Inline variables | `inline constexpr int MAX_PINS = 50;` | Avoids ODR issues with header-defined constants | +| `[[maybe_unused]]` | `[[maybe_unused]] int debug_only = 0;` | Suppresses unused-variable warnings cleanly | +| `[[fallthrough]]` | `case 1: doA(); [[fallthrough]]; case 2:` | Documents intentional switch fallthrough | +| Nested namespaces | `namespace wled::audio { }` | Shorter than nested `namespace` blocks | + +#### Patterns that break or change behavior + +| Pattern | GCC 8 behavior | GCC 14 behavior | Fix | +|---|---|---|---| +| `int x; enum E e = x;` | Warning (often ignored) | Error with `-Werror=enum-conversion` | `E e = static_cast(x);` | +| `int g;` in two `.cpp` files | Both compile, linker merges (tentative definition) | Error: multiple definitions (`-fno-common`) | `extern int g;` in header, `int g;` in one `.cpp` | +| `const char* ref = std::string(...).c_str();` | Silent dangling pointer | Warning (`-Wdangling-reference`) | Extend lifetime: store the `std::string` in a local variable | +| `register int x;` | Accepted (ignored) | Warning or error (`register` removed in C++17) | Remove `register` keyword | +| Narrowing in aggregate init | Warning | Error | Use explicit cast or wider type | +| Implicit `this` capture in lambdas | Accepted in `[=]` | Deprecated warning; error in C++20 mode | Use `[=, this]` or `[&]` | + + +#### Recommendations + +- **Do not raise the minimum C++ standard yet.** WLED must still build on IDF v4.4 (GCC 8.4, C++17). Use `#if __cplusplus > 201703L` to gate C++20 features. +- **Mark intentional fallthrough** with `[[fallthrough]]` — GCC 14 warns on unmarked fallthrough by default. + +- **Prefer `std::optional` over sentinel values** (e.g., `-1` for "no pin") in new code — it works on both compilers. +- **Use `std::string_view`** for read-only string parameters instead of `const char*` or `const String&` — zero-copy and works on GCC 8+. +- **Avoid raw `union` type punning** — prefer `memcpy` (GCC 8) or `std::bit_cast` (GCC 13+) for strict-aliasing safety. + + +### Deprecated and removed APIs + +#### RMT (Remote Control Transceiver) + +The legacy `rmt_*` functions are removed in IDF v5. Do not introduce new legacy RMT calls. + + +The new API is channel-based: + +| IDF v4 (legacy) | IDF v5 (new) | Notes | +|---|---|---| +| `rmt_config()` + `rmt_driver_install()` | `rmt_new_tx_channel()` / `rmt_new_rx_channel()` | Channels are now objects | +| `rmt_write_items()` | `rmt_transmit()` with encoder | Requires `rmt_encoder_t` | +| `rmt_set_idle_level()` | Configure in channel config | Set at creation time | +| `rmt_item32_t` | `rmt_symbol_word_t` | Different struct layout | + + +**WLED impact**: NeoPixelBus LED output and IR receiver both use legacy RMT. The upstream `V5-C6` branch adds `-D WLED_USE_SHARED_RMT` and disables IR until the library is ported. + +#### I2S (Inter-IC Sound) + +Legacy `i2s_driver_install()` + `i2s_read()` API is deprecated. When touching audio source code, wrap legacy I2S init and reading in `#if ESP_IDF_VERSION_MAJOR < 5` / `#else`. + + + The new API uses channel handles: +| IDF v4 (legacy) | IDF v5 (new) | Notes | +|---|---|---| +| `i2s_driver_install()` | `i2s_channel_init_std_mode()` | Separate STD/PDM/TDM modes | +| `i2s_set_pin()` | Pin config in `i2s_std_gpio_config_t` | Set at init time | +| `i2s_read()` | `i2s_channel_read()` | Uses channel handle | +| `i2s_set_clk()` | `i2s_channel_reconfig_std_clk()` | Reconfigure running channel | +| `i2s_config_t` | `i2s_std_config_t` | Separate config for each mode | + +**Migration pattern** (from Espressif examples): +```cpp +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) + #include "driver/i2s_std.h" + i2s_chan_handle_t rx_handle; + i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_0, I2S_ROLE_MASTER); + i2s_new_channel(&chan_cfg, NULL, &rx_handle); + + i2s_std_config_t std_cfg = { + .clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(22050), + .slot_cfg = I2S_STD_MSB_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_32BIT, I2S_SLOT_MODE_MONO), + .gpio_cfg = { .din = GPIO_NUM_32, .mclk = I2S_GPIO_UNUSED, ... }, + }; + i2s_channel_init_std_mode(rx_handle, &std_cfg); + i2s_channel_enable(rx_handle); +#else + // Legacy i2s_driver_install() path +#endif +``` + +**WLED impact**: The audioreactive usermod (`audio_source.h`) heavily uses legacy I2S. Migration requires rewriting the `I2SSource` class for channel-based API. + + +#### ADC (Analog-to-Digital Converter) + +Legacy `adc1_get_raw()` and `esp_adc_cal_*` are deprecated: + +| IDF v4 (legacy) | IDF v5 (new) | Notes | +|---|---|---| +| `adc1_config_width()` + `adc1_get_raw()` | `adc_oneshot_new_unit()` + `adc_oneshot_read()` | Object-based API | +| `esp_adc_cal_characterize()` | `adc_cali_create_scheme_*()` | Calibration is now scheme-based | +| `adc_continuous_*` (old) | `adc_continuous_*` (restructured) | Config struct changes | + +#### SPI Flash + +| IDF v4 (legacy) | IDF v5 (new) | +|---|---| +| `spi_flash_read()` | `esp_flash_read()` | +| `spi_flash_write()` | `esp_flash_write()` | +| `spi_flash_erase_range()` | `esp_flash_erase_region()` | + +WLED already has a compatibility shim in `ota_update.cpp` that maps old names to new ones. + +#### GPIO + +| IDF v4 (legacy) | IDF v5 (recommended) | +|---|---| +| `gpio_pad_select_gpio()` | `esp_rom_gpio_pad_select_gpio()` (or use `gpio_config()`) | +| `gpio_set_direction()` + `gpio_set_pull_mode()` | `gpio_config()` with `gpio_config_t` struct | + +### Features disabled in IDF v5 builds + +The upstream `V5-C6` branch explicitly disables features with incompatible library dependencies: + +```ini +# platformio.ini [esp32_idf_V5] +-D WLED_DISABLE_INFRARED # IR library uses legacy RMT +-D WLED_DISABLE_MQTT # AsyncMqttClient incompatible with IDF v5 +-D ESP32_ARDUINO_NO_RGB_BUILTIN # Prevents RMT driver conflict with built-in LED +-D WLED_USE_SHARED_RMT # Use new shared RMT driver for NeoPixel output +``` + + +### Migration checklist for new code + +1. **Never use a removed API without a version guard.** Always provide both old and new paths, or disable the feature on IDF v5. +2. **Test on both IDF v4.4 and v5.x builds** if the code must be backward-compatible. +3. **Prefer the newer API** when writing new code — wrap the old API in an `#else` block. +4. **Mark migration TODOs** with `// TODO(idf5):` so they are easy to find later. + +--- + +## Memory Management: `heap_caps_*` Best Practices + +ESP32 has multiple memory regions with different capabilities. Using the right allocator is critical for performance and stability. + + +### Memory regions + +| Region | Flag | Speed | DMA | Size | Use for | +|---|---|---|---|---|---| +| DRAM | `MALLOC_CAP_INTERNAL \| MALLOC_CAP_8BIT` | Fast | Yes (ESP32) | 200–320 KB | Hot-path buffers, task stacks, small allocations | +| IRAM | `MALLOC_CAP_EXEC` | Fastest | No | 32–128 KB | Code (automatic via `IRAM_ATTR`) | +| PSRAM (SPIRAM) | `MALLOC_CAP_SPIRAM \| MALLOC_CAP_8BIT` | Slower | Chip-dependent | 2–16 MB | Large buffers, JSON documents, image data | +| RTC RAM | `MALLOC_CAP_RTCRAM` | Moderate | No | 8 KB | Data surviving deep sleep; small persistent buffers | + + + +### WLED allocation wrappers + +WLED provides convenience wrappers with automatic fallback. **Always prefer these over raw `heap_caps_*` calls**: + +| Function | Allocation preference | Use case | +|---|---|---| +| `d_malloc(size)` | RTC → DRAM → PSRAM | General-purpose; prefers fast memory | +| `d_calloc(n, size)` | Same as `d_malloc`, zero-initialized | Arrays, structs | +| `p_malloc(size)` | PSRAM → DRAM | Large buffers; prefers abundant memory | +| `p_calloc(n, size)` | Same as `p_malloc`, zero-initialized | Large arrays | +| `d_malloc_only(size)` | RTC → DRAM (no PSRAM fallback) | DMA buffers, time-critical data | + +### PSRAM guidelines + +- **Check availability**: always test `psramFound()` before assuming PSRAM is present. +- **DMA compatibility**: on ESP32 (classic), PSRAM buffers are **not DMA-capable** — use `d_malloc_only()` to allocate DMA buffers in DRAM only. 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. +- **JSON documents**: use the `PSRAMDynamicJsonDocument` allocator (defined in `wled.h`) to put large JSON documents in PSRAM: + ```cpp + PSRAMDynamicJsonDocument doc(16384); // allocated in PSRAM if available + ``` +- **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. +- **Heap validation**: use `d_measureHeap()` and `d_measureContiguousFreeHeap()` to monitor remaining DRAM. Allocations that would drop free DRAM below `MIN_HEAP_SIZE` should go to PSRAM instead. +- **Performance**: Keep hot-path data in DRAM. Prefer PSRAM for capacity-oriented buffers and monitor contiguous DRAM headroom. + + PSRAM access is up to 15× slower than DRAM on ESP32, 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 at 80 MHz (120 MHz is possible with CONFIG_SPIRAM_SPEED_120M, which requires enabling experimental ESP-IDF features). 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. Keep hot-path data in DRAM regardless, but consider that ESP32 often crashes when the largest DRAM chunk gets below 10 KB. + + + +### Pattern: preference-based allocation + +When you need a buffer that works on boards with or without PSRAM: + +```cpp +// Prefer PSRAM for large buffers, fall back to DRAM +uint8_t* buf = (uint8_t*)heap_caps_malloc_prefer(bufSize, 2, + MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT, // first choice: PSRAM + MALLOC_CAP_DEFAULT); // fallback: any available +// Or simply: +uint8_t* buf = (uint8_t*)p_malloc(bufSize); +``` + + +--- + +## I2S Audio: Best Practices + +The audioreactive usermod uses I2S for microphone input. Key patterns: + +### Port selection + +```cpp +constexpr i2s_port_t AR_I2S_PORT = I2S_NUM_0; +// I2S_NUM_1 has limitations: no MCLK routing, no ADC support, no PDM support +``` + +Always use `I2S_NUM_0` unless you have a specific reason and have verified support on all target chips. + +### DMA buffer tuning + +DMA buffer size controls latency vs. reliability: + +| Scenario | `dma_buf_count` | `dma_buf_len` | Latency | Notes | +|---|---|---|---|---| +| With HUB75 matrix | 18 | 128 | ~100 ms | Higher count prevents I2S starvation during matrix DMA | +| Without PSRAM | 24 | 128 | ~140 ms | More buffers compensate for slower interrupt response | +| Default | 8 | 128 | ~46 ms | Acceptable for most setups | + +### Interrupt priority + +Choose interrupt priority based on coexistence with other drivers: + +```cpp +#ifdef WLED_ENABLE_HUB75MATRIX + .intr_alloc_flags = ESP_INTR_FLAG_IRAM | ESP_INTR_FLAG_LEVEL1, // level 1 (lowest) to avoid starving HUB75 +#else + .intr_alloc_flags = ESP_INTR_FLAG_LEVEL2 | ESP_INTR_FLAG_LEVEL3, // accept level 2 or 3 (allocator picks available) +#endif +``` + +### APLL (Audio PLL) usage + +The ESP32 has an audio PLL for precise sample rates. Rules: + +- Enable APLL when an MCLK pin is provided and precision matters. +- **Disable APLL** when Ethernet or HUB75 is active — they also use the APLL. +- APLL is broken on ESP32 revision 0 silicon. +- Not all chips have APLL — gate with `SOC_I2S_SUPPORTS_APLL`. + +```cpp +#if !defined(SOC_I2S_SUPPORTS_APLL) + _config.use_apll = false; +#elif defined(WLED_USE_ETHERNET) || defined(WLED_ENABLE_HUB75MATRIX) + _config.use_apll = false; // APLL conflict +#endif +``` + +### PDM microphone caveats + +- Not supported on ESP32-C3 (`SOC_I2S_SUPPORTS_PDM_RX` not defined). +- ESP32-S3 PDM has known issues: sample rate at 50% of expected, very low amplitude. + - **16-bit data width**: Espressif's IDF documentation states that in PDM mode the data unit width is always 16 bits, regardless of the configured `bits_per_sample`. + - See [espressif/esp-idf#8660](https://github.com/espressif/esp-idf/issues/8660) for the upstream issue. + - **Flag `bits_per_sample = I2S_BITS_PER_SAMPLE_32BIT` in PDM mode** — this causes the S3 low-amplitude symptom. +- No clock pin (`I2S_CKPIN = -1`) triggers PDM mode in WLED. + +--- + +## HUB75 LED Matrix: Best Practices + +WLED uses the `ESP32-HUB75-MatrixPanel-I2S-DMA` library for HUB75 matrix output. + +### Chip-specific panel limits + +```cpp +#if defined(CONFIG_IDF_TARGET_ESP32S3) && defined(BOARD_HAS_PSRAM) + maxChainLength = 6; // S3 + PSRAM: up to 6 panels (DMA-capable PSRAM) +#elif defined(CONFIG_IDF_TARGET_ESP32S2) + maxChainLength = 2; // S2: limited DMA channels +#else + maxChainLength = 4; // Classic ESP32: default +#endif +``` + +### Color depth vs. pixel count + +The driver dynamically reduces color depth for larger displays to stay within DMA buffer limits: + +| Pixel count | Color depth | Bits per pixel | +|---|---|---| +| ≤ `MAX_PIXELS_10BIT` | 10-bit (30-bit color) | High quality (experimental) | +| ≤ `MAX_PIXELS_8BIT` | 8-bit (24-bit color) | Full quality | +| ≤ `MAX_PIXELS_6BIT` | 6-bit (18-bit color) | Slight banding | +| ≤ `MAX_PIXELS_4BIT` | 4-bit (12-bit color) | Visible banding | +| larger | 3-bit (9-bit color) | Minimal color range | + +### Resource conflicts + +- **APLL**: HUB75 I2S DMA uses the APLL. Disable APLL in the audio I2S driver when HUB75 is active. +- **I2S peripheral**: HUB75 uses `I2S_NUM_1` (or `I2S_NUM_0` on single-I2S chips). Audio must use the other port. +- **Pin count**: HUB75 requires 13–14 GPIO pins. On ESP32-S2 this severely limits remaining GPIO. +- **Reboot required**: on ESP32-S3, changing HUB75 driver options requires a full reboot — the I2S DMA cannot be reconfigured at runtime. + +--- + + +## GPIO Best Practices + +### Prefer `gpio_config()` over individual calls + +```cpp +// Preferred: single struct-based configuration +gpio_config_t io_conf = { + .pin_bit_mask = (1ULL << pin), + .mode = GPIO_MODE_OUTPUT, + .pull_up_en = GPIO_PULLUP_DISABLE, + .pull_down_en = GPIO_PULLDOWN_DISABLE, + .intr_type = GPIO_INTR_DISABLE, +}; +gpio_config(&io_conf); + +// Avoid: multiple separate calls (more error-prone, deprecated in IDF v5) +gpio_set_direction(pin, GPIO_MODE_OUTPUT); +gpio_set_pull_mode(pin, GPIO_FLOATING); +``` + + +### Pin manager integration + +Always allocate pins through WLED's `pinManager` before using GPIO APIs: + +```cpp +if (!pinManager.allocatePin(myPin, true, PinOwner::UM_MyUsermod)) { + return; // pin in use by another module +} +// Now safe to configure +``` + +--- + +## Timer Best Practices + +### Microsecond timing + +For high-resolution timing, prefer `esp_timer_get_time()` (microsecond resolution, 64-bit) over `millis()` or `micros()`. + + +```cpp +#include +int64_t now_us = esp_timer_get_time(); // monotonic, not affected by NTP +``` + +> **Note**: In arduino-esp32, both `millis()` and `micros()` are thin wrappers around `esp_timer_get_time()` — they share the same monotonic clock source. Prefer the direct call when you need the full 64-bit value or ISR-safe access without truncation: +> ```cpp +> // arduino-esp32 internals (cores/esp32/esp32-hal-misc.c): +> // unsigned long micros() { return (unsigned long)(esp_timer_get_time()); } +> // unsigned long millis() { return (unsigned long)(esp_timer_get_time() / 1000ULL); } +> ``` + + +### Periodic timers + +For periodic tasks with sub-millisecond precision, use `esp_timer`: + +```cpp +esp_timer_handle_t timer; +esp_timer_create_args_t args = { + .callback = myCallback, + .arg = nullptr, + .dispatch_method = ESP_TIMER_TASK, // run in timer task (not ISR) + .name = "my_timer", +}; +esp_timer_create(&args, &timer); +esp_timer_start_periodic(timer, 1000); // 1 ms period +``` + + +Always prefer `ESP_TIMER_TASK` dispatch over `ESP_TIMER_ISR` unless you need ISR-level latency — ISR callbacks have severe restrictions (no logging, no heap allocation, no FreeRTOS API calls). + +### Precision waiting: coarse delay then spin-poll + +When waiting for a precise future deadline (e.g., FPS limiting, protocol timing), avoid spinning the entire duration — that wastes CPU and starves other tasks. Instead, yield to FreeRTOS while time allows, then spin only for the final window. + +```cpp +// Wait until 'target_us' (a micros() / esp_timer_get_time() timestamp) +long time_to_wait = (long)(target_us - micros()); +// Coarse phase: yield to FreeRTOS while we have more than ~2 ms remaining. +// vTaskDelay(1) suspends the task for one RTOS tick, letting other task run freely. +while (time_to_wait > 2000) { + vTaskDelay(1); + time_to_wait = (long)(target_us - micros()); +} +// Fine phase: busy-poll the last ≤2 ms for microsecond accuracy. +// micros() wraps esp_timer_get_time() so this is low-overhead. +while ((long)(target_us - micros()) > 0) { /* spin */ } +``` + + +> The threshold (2000 µs as an example) should be at least one RTOS tick (default 1 ms on ESP32) plus some margin. A value of 1500–3000 µs works well in practice. + +--- + +## ADC Best Practices + + +### Version-aware ADC code + +ADC is one of the most fragmented APIs across IDF versions: + +```cpp +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) + // IDF v5: oneshot driver + #include "esp_adc/adc_oneshot.h" + #include "esp_adc/adc_cali.h" + adc_oneshot_unit_handle_t adc_handle; + adc_oneshot_unit_init_cfg_t unit_cfg = { .unit_id = ADC_UNIT_1 }; + adc_oneshot_new_unit(&unit_cfg, &adc_handle); +#else + // IDF v4: legacy driver + #include "driver/adc.h" + #include "esp_adc_cal.h" + adc1_config_width(ADC_WIDTH_BIT_12); + int raw = adc1_get_raw(ADC1_CHANNEL_0); +#endif +``` + + +### Bit width portability + +Not all chips have 12-bit ADC. `SOC_ADC_MAX_BITWIDTH` reports the maximum resolution (12 or 13 bits). Note that in IDF v5, this macro was renamed to `CONFIG_SOC_ADC_RTC_MAX_BITWIDTH`. Write version-aware guards: + +```cpp +// IDF v4: SOC_ADC_MAX_BITWIDTH IDF v5: CONFIG_SOC_ADC_RTC_MAX_BITWIDTH +#if defined(CONFIG_SOC_ADC_RTC_MAX_BITWIDTH) // IDF v5+ + #define MY_ADC_MAX_BITWIDTH CONFIG_SOC_ADC_RTC_MAX_BITWIDTH +#elif defined(SOC_ADC_MAX_BITWIDTH) // IDF v4 + #define MY_ADC_MAX_BITWIDTH SOC_ADC_MAX_BITWIDTH +#else + #define MY_ADC_MAX_BITWIDTH 12 // safe fallback +#endif + +#if MY_ADC_MAX_BITWIDTH == 13 + adc1_config_width(ADC_WIDTH_BIT_13); // ESP32-S2 +#else + adc1_config_width(ADC_WIDTH_BIT_12); // ESP32, S3, C3, etc. +#endif +``` + +WLED's `util.cpp` uses the IDF v4 form (`SOC_ADC_MAX_BITWIDTH`) — this will need updating when the codebase migrates to IDF v5. + +--- + + +## RMT Best Practices + +### Current usage in WLED + +RMT drives NeoPixel LED output (via NeoPixelBus) and IR receiver input. Both use the legacy API that is removed in IDF v5. + +### Migration notes + +- The upstream `V5-C6` branch uses `-D WLED_USE_SHARED_RMT` to switch to the new RMT driver for NeoPixel output. +- IR is disabled on IDF v5 until the IR library is ported. +- New chips (C6, P4) have different RMT channel counts — use `SOC_RMT_TX_CANDIDATES_PER_GROUP` to check availability. +- The new RMT API requires an "encoder" object (`rmt_encoder_t`) to translate data formats — this is more flexible but requires more setup code. + + +--- + +## Espressif Best Practices (from official examples) + +### Error handling + +Always check `esp_err_t` return values. Use `ESP_ERROR_CHECK()` in initialization code, but handle errors gracefully in runtime code. + +```cpp +// Initialization — crash early on failure +ESP_ERROR_CHECK(i2s_driver_install(I2S_NUM_0, &config, 0, nullptr)); + +// Runtime — log and recover +esp_err_t err = i2s_read(I2S_NUM_0, buf, len, &bytes_read, portMAX_DELAY); +if (err != ESP_OK) { + DEBUGSR_PRINTF("I2S read failed: %s\n", esp_err_to_name(err)); + return; +} +``` + + +For situations between these two extremes — where you want the `ESP_ERROR_CHECK` formatted log message (file, line, error name) but must not abort — use `ESP_ERROR_CHECK_WITHOUT_ABORT()`. + + +```cpp +// Logs in the same format as ESP_ERROR_CHECK, but returns the error code instead of aborting. +// Useful for non-fatal driver calls where you want visibility without crashing. +esp_err_t err = ESP_ERROR_CHECK_WITHOUT_ABORT(i2s_set_clk(AR_I2S_PORT, rate, bits, ch)); +if (err != ESP_OK) return; // handle as needed +``` + + +### Logging + +WLED uses its own logging macros — **not** `ESP_LOGx()`. For application-level code, always use the WLED macros defined in `wled.h`: + +| Macro family | Defined in | Controlled by | Use for | +|---|---|---|---| +| `DEBUG_PRINT` / `DEBUG_PRINTLN` / `DEBUG_PRINTF` | `wled.h` | `WLED_DEBUG` build flag | Development/diagnostic output; compiled out in release builds | + +All of these wrap `Serial` output through the `DEBUGOUT` / `DEBUGOUTLN` / `DEBUGOUTF` macros. + +**Exception — low-level driver code**: When writing code that interacts directly with ESP-IDF APIs (e.g., I2S initialization, RMT setup), use `ESP_LOGx()` macros instead. They support tag-based filtering and compile-time log level control: + + +```cpp +static const char* TAG = "my_module"; +ESP_LOGI(TAG, "Initialized with %d buffers", count); +ESP_LOGW(TAG, "PSRAM not available, falling back to DRAM"); +ESP_LOGE(TAG, "Failed to allocate %u bytes", size); +``` + +### Task creation and pinning + + +On dual-core chips (ESP32, S3, P4), pin latency-sensitive tasks to a specific core: + +```cpp +xTaskCreatePinnedToCore( + audioTask, // function + "audio", // name + 4096, // stack size + nullptr, // parameter + 5, // priority (higher = more important) + &audioTaskHandle, // handle + 0 // core ID (0 = protocol core, 1 = app core) +); +``` + + +Guidelines: +- Pin network/protocol tasks to core 0 (where Wi-Fi runs). +- Pin real-time tasks (audio, LED output) to core 1. +- On single-core chips (S2, C3, C5, C6), only core 0 exists — pinning to core 1 will fail. Use `SOC_CPU_CORES_NUM > 1` guards or `tskNO_AFFINITY`. +- Use `SOC_CPU_CORES_NUM` to conditionally pin tasks: + ```cpp + #if SOC_CPU_CORES_NUM > 1 + xTaskCreatePinnedToCore(audioTask, "audio", 4096, nullptr, 5, &handle, 1); + #else + xTaskCreate(audioTask, "audio", 4096, nullptr, 5, &handle); + #endif + ``` + +**Tip: use xTaskCreateUniversal()** - from arduino-esp32 - to avoid the conditional on `SOC_CPU_CORES_NUM`. This function has the same signature as ``xTaskCreatePinnedToCore()``, but automatically falls back to ``xTaskCreate()`` on single-core MCUs. + +### `delay()`, `yield()`, and the IDLE task + +FreeRTOS on ESP32 is **preemptive** — all tasks are scheduled by priority regardless of `yield()` calls. This is fundamentally different from ESP8266 cooperative multitasking. + + +| Call | What it does | Reaches IDLE (priority 0)? | +|---|---|---| +| `delay(ms)` / `vTaskDelay(ticks)` | Suspends calling task; scheduler runs all other ready tasks | ✅ Yes | +| `yield()` / `vTaskDelay(0)` | Hint to switch to tasks at **equal or higher** priority only | ❌ No | +| `taskYIELD()` | Same as `vTaskDelay(0)` | ❌ No | +| Blocking API (`xQueueReceive`, `ulTaskNotifyTake`, `vTaskDelayUntil`) | Suspends task until event or timeout; IDLE runs freely | ✅ Yes | + + + +**`delay()` in `loopTask` is safe.** Arduino's `loop()` runs inside `loopTask`. Calling `delay()` suspends only `loopTask` — all other FreeRTOS tasks (Wi-Fi stack, audio FFT, LED DMA) continue uninterrupted on either core. + +**`yield()` does not yield to IDLE.** Any task that loops with only `yield()` calls will starve the IDLE task, causing the IDLE watchdog to fire. Always use `delay(1)` (or a blocking FreeRTOS call) in tight task loops. Note: WLED redefines `yield()` as an empty macro on ESP32 WLEDMM_FASTPATH builds. + +#### Why the IDLE task is not optional + + +The FreeRTOS IDLE task (one per core on dual-core ESP32 and ESP32-S3; single instance on single-core chips) is not idle in the casual sense — it performs essential system housekeeping: + +- **Frees deleted task memory**: when a task calls `vTaskDelete()`, the IDLE task reclaims its TCB and stack. Without IDLE running, deleted tasks leak memory permanently. +- **Runs the idle hook**: when `configUSE_IDLE_HOOK = 1`, the IDLE task calls `vApplicationIdleHook()` on every iteration — some ESP-IDF components register low-priority background work here. +- **Implements tickless idle / light sleep**: on battery-powered devices, IDLE is the entry point for low-power sleep. A permanently starved IDLE task disables light sleep entirely. +- **Runs registered idle hooks**: ESP-IDF components register callbacks via `esp_register_freertos_idle_hook()` (e.g., Wi-Fi background maintenance, Bluetooth housekeeping). These only fire when IDLE runs. + + + +In short: **starving IDLE corrupts memory cleanup, breaks background activities, disables low-power sleep, and prevents Wi-Fi/BT maintenance.** The IDLE watchdog panic is a symptom — the real damage happens before the watchdog fires. + +### Watchdog management + +Long-running operations may trigger the task watchdog. Feed it explicitly: + +```cpp +#include +esp_task_wdt_reset(); // feed the watchdog in long loops +``` + +For tasks that intentionally block for extended periods, consider subscribing/unsubscribing from the TWDT: + +```cpp +esp_task_wdt_delete(NULL); // remove current task from TWDT (IDF v4.4) +// ... long blocking operation ... +esp_task_wdt_add(NULL); // re-register +``` + +> **IDF v5 note**: In IDF v5, `esp_task_wdt_add()` and `esp_task_wdt_delete()` require an explicit `TaskHandle_t`. Use `xTaskGetCurrentTaskHandle()` instead of `NULL`. + + +--- + +## Quick Reference: IDF v4 → v5 API Mapping + +| Component | IDF v4 Header | IDF v5 Header | Key Change | +|---|---|---|---| +| I2S | `driver/i2s.h` | `driver/i2s_std.h` | Channel-based API | +| ADC (oneshot) | `driver/adc.h` | `esp_adc/adc_oneshot.h` | Unit/channel handles | +| ADC (calibration) | `esp_adc_cal.h` | `esp_adc/adc_cali.h` | Scheme-based calibration | +| RMT | `driver/rmt.h` | `driver/rmt_tx.h` / `rmt_rx.h` | Encoder-based transmit | +| SPI Flash | `spi_flash.h` | `esp_flash.h` | `esp_flash_*` functions | +| GPIO | `driver/gpio.h` | `driver/gpio.h` | `gpio_pad_select_gpio()` removed | +| Timer | `driver/timer.h` | `driver/gptimer.h` | General-purpose timer handles | +| PCNT | `driver/pcnt.h` | `driver/pulse_cnt.h` | Handle-based API | + diff --git a/docs/web.instructions.md b/docs/web.instructions.md new file mode 100644 index 0000000000..87c9b74531 --- /dev/null +++ b/docs/web.instructions.md @@ -0,0 +1,30 @@ +--- +applyTo: "wled00/data/**" +--- +# Web UI Coding Conventions + +## Formatting + +- Indent **HTML and JavaScript** with **tabs** +- Indent **CSS** with **tabs** + +## JavaScript Style + +- **camelCase** for functions and variables: `gId()`, `selectedFx`, `currentPreset` +- Abbreviated helpers are common: `d` for `document`, `gId()` for `getElementById()` + +## Key Files + +- `index.htm` — main interface +- `index.js` — functions that manage / update the main interface +- `settings*.htm` — configuration pages +- `*.css` — stylesheets (inlined during build) +- `common.js` — helper functions + +**Reuse shared helpers from `common.js` whenever possible** instead of duplicating utilities in page-local scripts. + +## Build Integration + +Files in this directory are processed by `tools/cdata.js` into generated headers +(`wled00/html_*.h`, `wled00/js_*.h`). +Run `npm run build` after any change. **Never edit generated headers directly.** diff --git a/package-lock.json b/package-lock.json index de0a4084f7..e7c90d648b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "wled", - "version": "16.0.0-alpha", + "version": "16.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "wled", - "version": "16.0.0-alpha", + "version": "16.0.0", "license": "ISC", "dependencies": { "clean-css": "^5.3.3", diff --git a/package.json b/package.json index 840dc5006f..c7af85c556 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wled", - "version": "16.0.0-alpha", + "version": "16.0.0", "description": "Tools for WLED project", "main": "tools/cdata.js", "directories": { diff --git a/pio-scripts/dynarray.py b/pio-scripts/dynarray.py index 2d3cfa90c5..7b580de08d 100644 --- a/pio-scripts/dynarray.py +++ b/pio-scripts/dynarray.py @@ -4,16 +4,77 @@ Import("env") from pathlib import Path -platform = env.get("PIOPLATFORM") -script_file = Path(f"tools/dynarray_{platform}.ld") -if script_file.is_file(): - linker_script = f"-T{script_file}" - if platform == "espressif32": - # For ESP32, the script must be added at the right point in the list - linkflags = env.get("LINKFLAGS", []) - idx = linkflags.index("memory.ld") - linkflags.insert(idx+1, linker_script) - env.Replace(LINKFLAGS=linkflags) +# Linker script fragment injected into the rodata output section of whichever +# platform we're building for. Placed just before the end-of-rodata marker so +# that the dynarray entries land in flash rodata and are correctly sorted. +DYNARRAY_INJECTION = ( + "\n /* dynarray: WLED dynamic module arrays */\n" + " . = ALIGN(0x10);\n" + " KEEP(*(SORT_BY_INIT_PRIORITY(.dynarray.*)))\n" + " " +) + + +def inject_before_marker(path, marker): + """Patch a linker script file in-place, inserting DYNARRAY_INJECTION before marker.""" + original = path.read_text() + path.write_text(original.replace(marker, DYNARRAY_INJECTION + marker, 1)) + + +if env.get("PIOPLATFORM") == "espressif32": + # Find sections.ld on the linker search path (LIBPATH). + sections_ld_path = None + for ld_dir in env.get("LIBPATH", []): + candidate = Path(str(ld_dir)) / "sections.ld" + if candidate.exists(): + sections_ld_path = candidate + break + + if sections_ld_path is not None: + # Inject inside the existing .flash.rodata output section, just before + # _rodata_end. IDF v5 enforces zero gaps between adjacent output + # sections via ASSERT statements, so INSERT AFTER .flash.rodata would + # fail. Injecting inside the section creates no new output section and + # leaves the ASSERTs satisfied. + build_dir = Path(env.subst("$BUILD_DIR")) + patched_path = build_dir / "dynarray_sections.ld" + import shutil + shutil.copy(sections_ld_path, patched_path) + inject_before_marker(patched_path, "_rodata_end = ABSOLUTE(.);") + + # Replace "sections.ld" in LINKFLAGS with an absolute path to our + # patched copy. The flag may appear as a bare token, combined as + # "-Tsections.ld", or split across two tokens ("-T", "sections.ld"). + patched_str = str(patched_path) + new_flags = [] + skip_next = False + for flag in env.get("LINKFLAGS", []): + if skip_next: + new_flags.append(patched_str if flag == "sections.ld" else flag) + skip_next = False + elif flag == "-T": + new_flags.append(flag) + skip_next = True + else: + new_flags.append(flag.replace("sections.ld", patched_str)) + env.Replace(LINKFLAGS=new_flags) else: - # For other platforms, put it in last - env.Append(LINKFLAGS=[linker_script]) + # Assume sections.ld will be built (ESP-IDF format); add a post-action to patch it + # TODO: consider using ESP-IDF linker fragment (https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-guides/linker-script-generation.html) + # For now, patch after building + sections_ld = Path(env.subst("$BUILD_DIR")) / "sections.ld" + def patch_sections_ld(target, source, env): + inject_before_marker(sections_ld, "_rodata_end = ABSOLUTE(.);") + env.AddPostAction(str(sections_ld), patch_sections_ld) + +elif env.get("PIOPLATFORM") == "espressif8266": + # The ESP8266 framework preprocesses eagle.app.v6.common.ld.h into + # local.eagle.app.v6.common.ld in $BUILD_DIR/ld/ at build time. Register + # a post-action on that generated file so the injection happens after + # C-preprocessing but before linking. + build_ld = Path(env.subst("$BUILD_DIR")) / "ld" / "local.eagle.app.v6.common.ld" + + def patch_esp8266_ld(target, source, env): + inject_before_marker(build_ld, "_irom0_text_end = ABSOLUTE(.);") + + env.AddPostAction(str(build_ld), patch_esp8266_ld) diff --git a/pio-scripts/validate_modules.py b/pio-scripts/validate_modules.py index ae098f43cc..cd2698b8ea 100644 --- a/pio-scripts/validate_modules.py +++ b/pio-scripts/validate_modules.py @@ -1,11 +1,12 @@ -import os import re import subprocess -from pathlib import Path # For OS-agnostic path manipulation +from pathlib import Path from click import secho from SCons.Script import Action, Exit Import("env") +_ATTR = re.compile(r'\bDW_AT_(name|comp_dir)\b') + def read_lines(p: Path): """ Read in the contents of a file for analysis """ @@ -13,86 +14,129 @@ def read_lines(p: Path): return f.readlines() -def _get_nm_path(env) -> str: - """ Derive the nm tool path from the build environment """ - if "NM" in env: - return env.subst("$NM") - # Derive from the C compiler: xtensa-esp32-elf-gcc → xtensa-esp32-elf-nm - cc = env.subst("$CC") - nm = re.sub(r'(gcc|g\+\+)$', 'nm', os.path.basename(cc)) - return os.path.join(os.path.dirname(cc), nm) +def _get_readelf_path(env) -> str: + """ Derive the readelf tool path from the build environment """ + # Derive from the C compiler: xtensa-esp32-elf-gcc → xtensa-esp32-elf-readelf + cc = Path(env.subst("$CC")) + return str(cc.with_name(re.sub(r'(gcc|g\+\+)$', 'readelf', cc.name))) def check_elf_modules(elf_path: Path, env, module_lib_builders) -> set[str]: - """ Check which modules have at least one defined symbol placed in the ELF. + """ Check which modules have at least one compilation unit in the ELF. The map file is not a reliable source for this: with LTO, original object file paths are replaced by temporary ltrans.o partitions in all output sections, making per-module attribution impossible from the map alone. - Instead we invoke nm --defined-only -l on the ELF, which uses DWARF debug - info to attribute each placed symbol to its original source file. - - Requires usermod libraries to be compiled with -g so that DWARF sections - are present in the ELF. load_usermods.py injects -g for all WLED modules - via dep.env.AppendUnique(CCFLAGS=["-g"]). + Instead we invoke readelf --debug-dump=info --dwarf-depth=1 on the ELF, + which reads only the top-level compilation-unit DIEs from .debug_info. + Each CU corresponds to one source file; matching DW_AT_comp_dir + + DW_AT_name against the module src_dirs is sufficient to confirm a module + was compiled into the ELF. The output volume is proportional to the + number of source files, not the number of symbols. Returns the set of build_dir basenames for confirmed modules. """ - nm_path = _get_nm_path(env) + readelf_path = _get_readelf_path(env) + secho(f"INFO: Checking for usermod compilation units...") + try: result = subprocess.run( - [nm_path, "--defined-only", "-l", str(elf_path)], + [readelf_path, "--debug-dump=info", "--dwarf-depth=1", str(elf_path)], capture_output=True, text=True, errors="ignore", timeout=120, ) - nm_output = result.stdout + output = result.stdout except (subprocess.TimeoutExpired, FileNotFoundError, OSError) as e: - secho(f"WARNING: nm failed ({e}); skipping per-module validation", fg="yellow", err=True) + secho(f"WARNING: readelf failed ({e}); skipping per-module validation", fg="yellow", err=True) return {Path(b.build_dir).name for b in module_lib_builders} # conservative pass - # Match placed symbols against builders as we parse nm output, exiting early - # once all builders are accounted for. - # nm --defined-only still includes debugging symbols (type 'N') such as the - # per-CU markers GCC emits in .debug_info (e.g. "usermod_example_cpp_6734d48d"). - # These live at address 0x00000000 in their debug section — not in any load - # segment — so filtering them out leaves only genuinely placed symbols. - # nm -l appends a tab-separated "file:lineno" location to each symbol line. remaining = {Path(str(b.src_dir)): Path(b.build_dir).name for b in module_lib_builders} found = set() - - for line in nm_output.splitlines(): - if not remaining: - break # all builders matched - addr, _, _ = line.partition(' ') - if not addr.lstrip('0'): - continue # zero address — skip debug-section marker - if '\t' not in line: - continue - loc = line.rsplit('\t', 1)[1] - # Strip trailing :lineno (e.g. "/path/to/foo.cpp:42" → "/path/to/foo.cpp") - src_path = Path(loc.rsplit(':', 1)[0]) - # Path.is_relative_to() handles OS-specific separators correctly without - # any regex, avoiding Windows path escaping issues. + project_dir = Path(env.subst("$PROJECT_DIR")) + + def _flush_cu(comp_dir: str | None, name: str | None) -> None: + """Match one completed CU against remaining builders.""" + if not name or not remaining: + return + p = Path(name) + src_path = (Path(comp_dir) / p) if (comp_dir and not p.is_absolute()) else p + # In arduino+espidf dual-framework builds the IDF toolchain sets DW_AT_comp_dir + # to the virtual path "/IDF_PROJECT" rather than the real project root, so + # src_path won't match. Pre-compute a fallback using $PROJECT_DIR and check + # both candidates in a single pass. + use_fallback = not p.is_absolute() and comp_dir and Path(comp_dir) != project_dir + src_path_real = project_dir / p if use_fallback else None for src_dir in list(remaining): - if src_path.is_relative_to(src_dir): + if src_path.is_relative_to(src_dir) or (src_path_real and src_path_real.is_relative_to(src_dir)): found.add(remaining.pop(src_dir)) - break + return + + # readelf emits one DW_TAG_compile_unit DIE per source file. Attributes + # of interest: + # DW_AT_name — source file (absolute, or relative to comp_dir) + # DW_AT_comp_dir — compile working directory + # Both appear as either a direct string or an indirect string: + # DW_AT_name : foo.cpp + # DW_AT_name : (indirect string, offset: 0x…): foo.cpp + # Taking the portion after the *last* ": " on the line handles both forms. + + comp_dir = name = None + for line in output.splitlines(): + if 'Compilation Unit @' in line: + _flush_cu(comp_dir, name) + comp_dir = name = None + continue + if not remaining: + break # all builders matched + m = _ATTR.search(line) + if m: + _, _, val = line.rpartition(': ') + val = val.strip() + if m.group(1) == 'name': + name = val + else: + comp_dir = val + _flush_cu(comp_dir, name) # flush the last CU return found -DYNARRAY_SECTION = ".dtors" if env.get("PIOPLATFORM") == "espressif8266" else ".dynarray" -USERMODS_SECTION = f"{DYNARRAY_SECTION}.usermods.1" - def count_usermod_objects(map_file: list[str]) -> int: - """ Returns the number of usermod objects in the usermod list """ - # Count the number of entries in the usermods table section - return len([x for x in map_file if USERMODS_SECTION in x]) + """ Returns the number of usermod objects in the usermod list. + + Computes the count from the address span between the .dynarray.usermods.0 + and .dynarray.usermods.99999 sentinel sections. This mirrors the + DYNARRAY_LENGTH macro and is reliable under LTO, where all entries are + merged into a single ltrans partition so counting section occurrences + always yields 1 regardless of the true count. + """ + ENTRY_SIZE = 4 # sizeof(Usermod*) on 32-bit targets + addr_begin = None + addr_end = None + + for i, line in enumerate(map_file): + stripped = line.strip() + if stripped == '.dynarray.usermods.0': + if i + 1 < len(map_file): + m = re.search(r'0x([0-9a-fA-F]+)', map_file[i + 1]) + if m: + addr_begin = int(m.group(1), 16) + elif stripped == '.dynarray.usermods.99999': + if i + 1 < len(map_file): + m = re.search(r'0x([0-9a-fA-F]+)', map_file[i + 1]) + if m: + addr_end = int(m.group(1), 16) + if addr_begin is not None and addr_end is not None: + break + + if addr_begin is None or addr_end is None: + return 0 + return (addr_end - addr_begin) // ENTRY_SIZE def validate_map_file(source, target, env): """ Validate that all modules appear in the output build """ build_dir = Path(env.subst("$BUILD_DIR")) - map_file_path = build_dir / env.subst("${PROGNAME}.map") + map_file_path = build_dir / env.subst("${PROGNAME}.map") if not map_file_path.exists(): secho(f"ERROR: Map file not found: {map_file_path}", fg="red", err=True) @@ -111,6 +155,7 @@ def validate_map_file(source, target, env): secho(f"INFO: {usermod_object_count} usermod object entries") elf_path = build_dir / env.subst("${PROGNAME}.elf") + confirmed_modules = check_elf_modules(elf_path, env, module_lib_builders) missing_modules = [modname for mdir, modname in modules.items() if mdir not in confirmed_modules] @@ -120,7 +165,6 @@ def validate_map_file(source, target, env): fg="red", err=True) Exit(1) - return None env.Append(LINKFLAGS=[env.subst("-Wl,--Map=${BUILD_DIR}/${PROGNAME}.map")]) env.AddPostAction("$BUILD_DIR/${PROGNAME}.elf", Action(validate_map_file, cmdstr='Checking linked optional modules (usermods) in map file')) diff --git a/platformio.ini b/platformio.ini index f3f5d61b06..d29056825f 100644 --- a/platformio.ini +++ b/platformio.ini @@ -38,6 +38,7 @@ data_dir = ./wled00/data build_cache_dir = ~/.buildcache extra_configs = platformio_override.ini + platformio_release.ini [common] # ------------------------------------------------------------------------------ @@ -221,6 +222,7 @@ lib_deps = ESPAsyncTCP @ 1.2.2 ESPAsyncUDP ESP8266PWM + https://github.com/tignioj/ArduinoUZlib.git#20aff95cd80c141f80bdbf66895409a0046d2c2f ${env.lib_deps} ;; compatibilty flags - same as 0.14.0 which seems to work better on some 8266 boards. Not using PIO_FRAMEWORK_ARDUINO_MMU_CACHE16_IRAM48 @@ -251,6 +253,7 @@ lib_deps_compat = IRremoteESP8266 @ 2.8.2 makuna/NeoPixelBus @ 2.7.9 https://github.com/blazoncek/QuickESPNow.git#optional-debug + https://github.com/tignioj/ArduinoUZlib.git#20aff95cd80c141f80bdbf66895409a0046d2c2f https://github.com/Aircoookie/ESPAsyncWebServer.git#v2.4.0 [esp32_all_variants] @@ -375,6 +378,7 @@ build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME= -D WLED_DISABLE_PARTICLESYSTEM2D lib_deps = ${esp8266.lib_deps} monitor_filters = esp8266_exception_decoder +custom_usermods = audioreactive [env:nodemcuv2_compat] extends = env:nodemcuv2 @@ -384,6 +388,7 @@ platform_packages = ${esp8266.platform_packages_compat} build_flags = ${common.build_flags} ${esp8266.build_flags_compat} -D WLED_RELEASE_NAME=\"ESP8266_compat\" #-DWLED_DISABLE_2D -D WLED_DISABLE_PARTICLESYSTEM2D ;; lib_deps = ${esp8266.lib_deps_compat} ;; experimental - use older NeoPixelBus 2.7.9 +custom_usermods = audioreactive [env:nodemcuv2_160] extends = env:nodemcuv2 @@ -402,6 +407,7 @@ build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME= -D WLED_DISABLE_PARTICLESYSTEM2D -D WLED_DISABLE_PARTICLESYSTEM1D lib_deps = ${esp8266.lib_deps} +custom_usermods = audioreactive [env:esp8266_2m_compat] extends = env:esp8266_2m @@ -411,6 +417,7 @@ platform_packages = ${esp8266.platform_packages_compat} build_flags = ${common.build_flags} ${esp8266.build_flags_compat} -D WLED_RELEASE_NAME=\"ESP02_compat\" #-DWLED_DISABLE_2D -D WLED_DISABLE_PARTICLESYSTEM1D -D WLED_DISABLE_PARTICLESYSTEM2D +custom_usermods = audioreactive [env:esp8266_2m_160] extends = env:esp8266_2m @@ -713,3 +720,89 @@ monitor_filters = esp32_exception_decoder board_build.flash_mode = dio custom_usermods = * ; Expands to all usermods in usermods folder board_build.partitions = ${esp32.extreme_partitions} ; We're gonna need a bigger boat + + + +# ------------------------------------------------------------------------------ +# Hub75 examples +# ------------------------------------------------------------------------------ +# Note: some panels may experience ghosting with default full brightness. use -D WLED_HUB75_MAX_BRIGHTNESS=239 or lower to fix it. + +[hub75] +;; Shared values for all HUB75 build envs. + +;; Core HUB75 flags - common to every HUB75 build +build_flags = + -D WLED_ENABLE_HUB75MATRIX -D NO_GFX + -D WLED_DEBUG_BUS + -D LED_TYPES=TYPE_HUB75MATRIX_HS + ; -D WLED_DEBUG + +;; Default I2S mic pins disabled (HUB75 uses GPIOs that would otherwise clash). +;; Envs that wire up a real mic should NOT include this and define I2S_*PIN themselves. +i2s_disable_flags = -D SR_DMTYPE=1 -D I2S_SDPIN=-1 -D I2S_CKPIN=-1 -D I2S_WSPIN=-1 -D MCLK_PIN=-1 ;; Disable to prevent pin clash + +;; Pinned HUB75 driver libraries +lib_deps = https://github.com/mrfaptastic/ESP32-HUB75-MatrixPanel-DMA.git#f17fb7fe9d487e9643f919eb5aeedea8d9d1f8d7 ;; 3.0.14 + +;; Extra flags shared by all S3-based HUB75 builds +s3_build_flags = + -DARDUINO_USB_CDC_ON_BOOT=1 ;; -DARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB") + -DBOARD_HAS_PSRAM + -DLOLIN_WIFI_FIX ; seems to work much better with this (sets lower TX power) + -D WLED_WATCHDOG_TIMEOUT=0 + -D S3_LCD_DIV_NUM=20 ;; Attempt to fix wifi performance issue when panel active with S3 chips + + +[env:esp32dev_hub75] +extends = env:esp32dev +upload_speed = 921600 +build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} ${hub75.build_flags} ${hub75.i2s_disable_flags} + -DARDUINO_USB_CDC_ON_BOOT=0 ;; this flag is mandatory for "classic ESP32" when building with arduino-esp32 >=2.0.3 + -D WLED_RELEASE_NAME=\"ESP32_HUB75\" +lib_deps = ${esp32_idf_V4.lib_deps} + ${hub75.lib_deps} + +[env:esp32dev_hub75_forum_pinout] +extends = env:esp32dev_hub75 +build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} ${hub75.build_flags} ${hub75.i2s_disable_flags} + -DARDUINO_USB_CDC_ON_BOOT=0 + -D WLED_RELEASE_NAME=\"ESP32_HUB75_forum_pinout\" + -D ESP32_FORUM_PINOUT ;; enable for SmartMatrix default pins + +[env:esp32s3dev_4MB_qspi_hub75] +; HD-WF2 - NOTE: this board has NO PSRAM, so BOARD_HAS_PSRAM must not be set +; (BOARD_HAS_PSRAM causes the DMA library to allocate only in SPIRAM, which fails without PSRAM) +extends = env:esp32s3dev_8MB_qspi +board_build.partitions = ${esp32.extended_partitions} ;; 1.65MB firmware, 700KB filesystem +build_unflags = -DBOARD_HAS_PSRAM +build_flags = ${common.build_flags} ${esp32s3.build_flags} ${hub75.build_flags} ${hub75.s3_build_flags} ${hub75.i2s_disable_flags} + -D WLED_RELEASE_NAME=\"ESP32-S3_HD-WF2\" + -D HD_WF2_PINOUT ;; Huidu HD-WF2 specific GPIO wiring +lib_deps = ${esp32s3.lib_deps} + ${hub75.lib_deps} + +[env:adafruit_matrixportal_esp32s3] +; ESP32-S3 processor, 8 MB flash, 2 MB of PSRAM, dedicated driver pins for HUB75 +extends = env:esp32s3dev_8MB_qspi +board = adafruit_matrixportal_esp32s3_wled ; modified board definition: removed flash section that causes FS erase on upload +build_flags = ${common.build_flags} ${esp32s3.build_flags} ${hub75.build_flags} ${hub75.s3_build_flags} ${hub75.i2s_disable_flags} + -D WLED_RELEASE_NAME=\"ESP32-S3_Adafruit_Matrixportal\" + -D ARDUINO_ADAFRUIT_MATRIXPORTAL_ESP32S3 +lib_deps = ${esp32s3.lib_deps} + ${hub75.lib_deps} +;; board_build.partitions = tools/partitions-8MB_spiffs-tinyuf2.csv ;; supports adafruit UF2 bootloader + +[env:esp32s3dev_16MB_opi_hub75] +;; MOONHUB HUB75 adapter board (lilygo T7-S3 with 16MB flash and octal PSRAM) +extends = env:esp32s3dev_8MB_opi +board = lilygo-t7-s3 +board_build.partitions = ${esp32.extreme_partitions} ;; for 16MB flash (overrides large_partitions for 8MB) +;; Note: real I2S mic pins are wired here, so we do NOT include ${hub75.i2s_disable_flags}. +build_flags = ${common.build_flags} ${esp32s3.build_flags} ${hub75.build_flags} ${hub75.s3_build_flags} + -D WLED_RELEASE_NAME=\"ESP32-S3_16MB_opi_HUB75\" + -D MOONHUB_S3_PINOUT ;; HUB75 pinout + -D LEDPIN=14 -D BTNPIN=0 -D RLYPIN=15 -D IRPIN=-1 -D AUDIOPIN=-1 ;; defaults that avoid pin conflicts with HUB75 + -D SR_DMTYPE=1 -D I2S_SDPIN=10 -D I2S_CKPIN=11 -D I2S_WSPIN=12 -D MCLK_PIN=-1 ;; I2S mic +lib_deps = ${esp32s3.lib_deps} + ${hub75.lib_deps} diff --git a/platformio_override.sample.ini b/platformio_override.sample.ini index bf7a1314d6..bd34806938 100644 --- a/platformio_override.sample.ini +++ b/platformio_override.sample.ini @@ -193,7 +193,8 @@ build_flags = ${common.build_flags} ${esp8266.build_flags} # ------------------------------------------------------------------------------ # Optional: build flags for speed, instead of optimising for size. -# Example of usage: see [env:esp32S3_PSRAM_HUB75] +# Add ${Speed_Flags.build_flags} / ${Speed_Flags.build_unflags} to your own env +# in platformio_override.ini to opt in. # ------------------------------------------------------------------------------ [Speed_Flags] @@ -554,102 +555,4 @@ custom_usermods = https://github.com/wled/wled-usermod-example.git#main -# ------------------------------------------------------------------------------ -# Hub75 examples -# ------------------------------------------------------------------------------ -# Note: some panels may experience ghosting with default full brightness. use -D WLED_HUB75_MAX_BRIGHTNESS=239 or lower to fix it. - -[env:esp32dev_hub75] -board = esp32dev -upload_speed = 921600 -platform = ${esp32_idf_V4.platform} -platform_packages = -build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags} - -D WLED_RELEASE_NAME=\"ESP32_hub75\" - -D WLED_ENABLE_HUB75MATRIX -D NO_GFX - -D WLED_DEBUG_BUS - ; -D WLED_DEBUG - -D SR_DMTYPE=1 -D I2S_SDPIN=-1 -D I2S_CKPIN=-1 -D I2S_WSPIN=-1 -D MCLK_PIN=-1 ;; Disable to prevent pin clash - -lib_deps = ${esp32_idf_V4.lib_deps} - https://github.com/mrfaptastic/ESP32-HUB75-MatrixPanel-DMA.git#3.0.11 - -monitor_filters = esp32_exception_decoder -board_build.partitions = ${esp32.default_partitions} -board_build.flash_mode = dio -custom_usermods = audioreactive - -[env:esp32dev_hub75_forum_pinout] -extends = env:esp32dev_hub75 -build_flags = ${common.build_flags} - -D WLED_RELEASE_NAME=\"ESP32_hub75_forum_pinout\" - -D WLED_ENABLE_HUB75MATRIX -D NO_GFX - -D ESP32_FORUM_PINOUT ;; enable for SmartMatrix default pins - -D WLED_DEBUG_BUS - -D SR_DMTYPE=1 -D I2S_SDPIN=-1 -D I2S_CKPIN=-1 -D I2S_WSPIN=-1 -D MCLK_PIN=-1 ;; Disable to prevent pin clash -; -D WLED_DEBUG - - -[env:adafruit_matrixportal_esp32s3] -; ESP32-S3 processor, 8 MB flash, 2 MB of PSRAM, dedicated driver pins for HUB75 -board = adafruit_matrixportal_esp32s3_wled ; modified board definition: removed flash section that causes FS erase on upload -;; adafruit recommends to use arduino-esp32 2.0.14 -;;platform = espressif32@ ~6.5.0 -;;platform_packages = platformio/framework-arduinoespressif32 @ 3.20014.231204 ;; arduino-esp32 2.0.14 -platform = ${esp32s3.platform} -platform_packages = -upload_speed = 921600 -build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_8M_qspi\" - -DARDUINO_USB_CDC_ON_BOOT=1 ;; -DARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB") - -DBOARD_HAS_PSRAM - -DLOLIN_WIFI_FIX ; seems to work much better with this (sets lower TX power) - -D WLED_WATCHDOG_TIMEOUT=0 - -D WLED_ENABLE_HUB75MATRIX -D NO_GFX - -D S3_LCD_DIV_NUM=20 ;; Attempt to fix wifi performance issue when panel active with S3 chips - -D ARDUINO_ADAFRUIT_MATRIXPORTAL_ESP32S3 - -D WLED_DEBUG_BUS - -D SR_DMTYPE=1 -D I2S_SDPIN=-1 -D I2S_CKPIN=-1 -D I2S_WSPIN=-1 -D MCLK_PIN=-1 ;; Disable to prevent pin clash - - -lib_deps = ${esp32s3.lib_deps} - https://github.com/mrfaptastic/ESP32-HUB75-MatrixPanel-DMA.git#aa28e2a ;; S3_LCD_DIV_NUM fix -board_build.partitions = ${esp32.large_partitions} ;; standard bootloader and 8MB Flash partitions -;; board_build.partitions = tools/partitions-8MB_spiffs-tinyuf2.csv ;; supports adafruit UF2 bootloader -board_build.f_flash = 80000000L -board_build.flash_mode = qio -monitor_filters = esp32_exception_decoder -custom_usermods = audioreactive - -[env:esp32S3_PSRAM_HUB75] -;; MOONHUB HUB75 adapter board (lilygo T7-S3 with 16MB flash and PSRAM) -board = lilygo-t7-s3 -platform = ${esp32s3.platform} -platform_packages = -upload_speed = 921600 -build_unflags = ${common.build_unflags} - ${Speed_Flags.build_unflags} ;; optional: removes "-Os" so we can override with "-O2" in build_flags -build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"esp32S3_16MB_PSRAM_HUB75\" - ${Speed_Flags.build_flags} ;; optional: -O2 -> optimize for speed instead of size - -DARDUINO_USB_CDC_ON_BOOT=1 ;; -DARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB") - -DBOARD_HAS_PSRAM - -DLOLIN_WIFI_FIX ; seems to work much better with this (sets lower TX power) - -D WLED_WATCHDOG_TIMEOUT=0 - -D WLED_ENABLE_HUB75MATRIX -D NO_GFX - -D S3_LCD_DIV_NUM=20 ;; Attempt to fix wifi performance issue when panel active with S3 chips - -D MOONHUB_S3_PINOUT ;; HUB75 pinout - -D WLED_DEBUG_BUS - -D LEDPIN=14 -D BTNPIN=0 -D RLYPIN=15 -D IRPIN=-1 -D AUDIOPIN=-1 ;; defaults that avoid pin conflicts with HUB75 - -D SR_DMTYPE=1 -D I2S_SDPIN=10 -D I2S_CKPIN=11 -D I2S_WSPIN=12 -D MCLK_PIN=-1 ;; I2S mic - -lib_deps = ${esp32s3.lib_deps} - https://github.com/mrfaptastic/ESP32-HUB75-MatrixPanel-DMA.git#aa28e2a ;; S3_LCD_DIV_NUM fix - -;;board_build.partitions = ${esp32.large_partitions} ;; for 8MB flash -board_build.partitions = ${esp32.extreme_partitions} ;; for 16MB flash -board_build.f_flash = 80000000L -board_build.flash_mode = qio -monitor_filters = esp32_exception_decoder -custom_usermods = audioreactive \ No newline at end of file diff --git a/tools/dynarray_espressif32.ld b/tools/dynarray_espressif32.ld deleted file mode 100644 index 70ce51f19c..0000000000 --- a/tools/dynarray_espressif32.ld +++ /dev/null @@ -1,10 +0,0 @@ -/* ESP32 linker script fragment to add dynamic array section to binary */ -SECTIONS -{ - .dynarray : - { - . = ALIGN(0x10); - KEEP(*(SORT_BY_INIT_PRIORITY(.dynarray.*))) - } > default_rodata_seg -} -INSERT AFTER .flash.rodata; diff --git a/usermods/Fix_unreachable_netservices_v2/library.json b/usermods/Fix_unreachable_netservices_v2/library.json index 4d1dbfc8e4..51bdd3bbf2 100644 --- a/usermods/Fix_unreachable_netservices_v2/library.json +++ b/usermods/Fix_unreachable_netservices_v2/library.json @@ -1,4 +1,5 @@ { "name": "Fix_unreachable_netservices_v2", + "build": { "libArchive": false }, "platforms": ["espressif8266"] } diff --git a/usermods/Fix_unreachable_netservices_v2/usermod_Fix_unreachable_netservices.cpp b/usermods/Fix_unreachable_netservices_v2/usermod_Fix_unreachable_netservices.cpp index 7fb8e97982..47ddea3135 100644 --- a/usermods/Fix_unreachable_netservices_v2/usermod_Fix_unreachable_netservices.cpp +++ b/usermods/Fix_unreachable_netservices_v2/usermod_Fix_unreachable_netservices.cpp @@ -77,7 +77,7 @@ class FixUnreachableNetServices : public Usermod } if (m_updateConfig) { - serializeConfig(); + serializeConfigToFS(); m_updateConfig = false; } } diff --git a/usermods/audioreactive/audio_reactive.cpp b/usermods/audioreactive/audio_reactive.cpp index 757ad35482..ce0f990359 100644 --- a/usermods/audioreactive/audio_reactive.cpp +++ b/usermods/audioreactive/audio_reactive.cpp @@ -140,7 +140,7 @@ static uint8_t binNum = 8; // Used to select the bin for FFT based bea #if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) #define UM_AUDIOREACTIVE_USE_INTEGER_FFT // always use integer FFT on ESP32-S2 and ESP32-C3 #endif -#endif +#endif // UM_AUDIOREACTIVE_USE_ARDUINO_FFT #if !defined(UM_AUDIOREACTIVE_USE_INTEGER_FFT) using FFTsampleType = float; @@ -296,17 +296,17 @@ void FFTcode(void * parameter) ArduinoFFT FFT = ArduinoFFT(valFFT, vImag, samplesFFT, SAMPLE_RATE, true); #elif !defined(UM_AUDIOREACTIVE_USE_INTEGER_FFT) // allocate and initialize FFT buffers on first call - // note: free() is never used on these pointers. If it ever is implemented, this implementation can cause memory leaks (need to free raw pointers) if (valFFT == nullptr) { - float* raw_buffer = (float*)heap_caps_malloc((2 * samplesFFT * sizeof(float)) + 16, MALLOC_CAP_8BIT); - if ((raw_buffer == nullptr)) return; // something went wrong - valFFT = (float*)(((uintptr_t)raw_buffer + 15) & ~15); // SIMD requires aligned memory to 16-byte boundary. note in IDF5 there is MALLOC_CAP_SIMD available + valFFT = (float*)heap_caps_aligned_calloc(16, 2 * samplesFFT, sizeof(float), MALLOC_CAP_8BIT); // SIMD requires aligned memory to 16-byte boundary. note in IDF5 there is MALLOC_CAP_SIMD available + if ((valFFT == nullptr)) return; // something went wrong } // create window if (windowFFT == nullptr) { - float* raw_buffer = (float*)heap_caps_malloc((samplesFFT * sizeof(float)) + 16, MALLOC_CAP_8BIT); - if ((raw_buffer == nullptr)) return; // something went wrong - windowFFT = (float*)(((uintptr_t)raw_buffer + 15) & ~15); // SIMD requires aligned memory to 16-byte boundary + windowFFT = (float*)heap_caps_aligned_calloc(16, samplesFFT, sizeof(float), MALLOC_CAP_8BIT); // SIMD requires aligned memory to 16-byte boundary. note in IDF5 there is MALLOC_CAP_SIMD available + if ((windowFFT == nullptr)) { + heap_caps_free(valFFT); valFFT = nullptr; + return; // something went wrong + } } if (dsps_fft2r_init_fc32(NULL, samplesFFT) != ESP_OK) return; // initialize FFT tables // create window function for FFT @@ -316,16 +316,20 @@ void FFTcode(void * parameter) dsps_wind_flat_top_f32(windowFFT, samplesFFT); #endif #else - // allocate and initialize integer FFT buffers on first call - if (valFFT == nullptr) valFFT = (int16_t*) calloc(sizeof(int16_t), samplesFFT * 2); - if ((valFFT == nullptr)) return; // something went wrong + // use integer FFT - allocate and initialize integer FFT buffers on first call, 4 bytes aligned (just in case, even if not strictly needed for int16_t) + if (valFFT == nullptr) valFFT = (int16_t*) heap_caps_aligned_calloc(4, samplesFFT * 2, sizeof(int16_t), MALLOC_CAP_8BIT); // create window - if (windowFFT == nullptr) windowFFT = (int16_t*) calloc(sizeof(int16_t), samplesFFT); - if ((windowFFT == nullptr)) return; // something went wrong - if (dsps_fft2r_init_sc16(NULL, samplesFFT) != ESP_OK) return; // initialize FFT tables + if (windowFFT == nullptr) windowFFT = (int16_t*) heap_caps_aligned_calloc(4, samplesFFT, sizeof(int16_t), MALLOC_CAP_8BIT); // create window function for FFT - float *windowFloat = (float*) calloc(sizeof(float), samplesFFT); // temporary buffer for window function - if ((windowFloat == nullptr)) return; // something went wrong + float *windowFloat = (float*) heap_caps_aligned_calloc(4, samplesFFT, sizeof(float), MALLOC_CAP_8BIT); // temporary buffer for window function + if (windowFloat == nullptr || windowFFT == nullptr || valFFT == nullptr) { // something went wrong + if (windowFloat) heap_caps_free(windowFloat); + if (windowFFT) heap_caps_free(windowFFT); windowFFT = nullptr; + if (valFFT) heap_caps_free(valFFT); valFFT = nullptr; + return; + } + if (dsps_fft2r_init_sc16(NULL, samplesFFT) != ESP_OK) return; // initialize FFT tables + #ifdef FFT_PREFER_EXACT_PEAKS dsps_wind_blackman_harris_f32(windowFloat, samplesFFT); #else @@ -335,7 +339,7 @@ void FFTcode(void * parameter) for (int i = 0; i < samplesFFT; i++) { windowFFT[i] = (int16_t)(windowFloat[i] * 32767.0f); } - free(windowFloat); // free temporary buffer + heap_caps_free(windowFloat); // free temporary buffer #endif // see https://www.freertos.org/vtaskdelayuntil.html @@ -468,7 +472,6 @@ void FFTcode(void * parameter) } FFT_Magnitude = FFT_Magnitude_int * 512; // scale to match raw float value FFT_MajorPeak = FFT_MajorPeak_int; - FFT_Magnitude = FFT_Magnitude_int; #endif #endif FFT_MajorPeak = constrain(FFT_MajorPeak, 1.0f, 11025.0f); // restrict value to range expected by effects @@ -758,6 +761,8 @@ class AudioReactive : public Usermod { private: #ifdef ARDUINO_ARCH_ESP32 + static constexpr uint8_t SR_DMTYPE_NETWORK_ONLY = 254; + #ifndef AUDIOPIN int8_t audioPin = -1; #else @@ -879,6 +884,9 @@ class AudioReactive : public Usermod { #endif static const char _digitalmic[]; static const char _addPalettes[]; + static const char _palName0[]; + static const char _palName1[]; + static const char _palName2[]; static const char UDP_SYNC_HEADER[]; static const char UDP_SYNC_HEADER_v1[]; @@ -1439,7 +1447,7 @@ class AudioReactive : public Usermod { break; #endif - case 254: // dummy "network receive only" mode + case SR_DMTYPE_NETWORK_ONLY: // dummy "network receive only" mode if (audioSource) delete audioSource; audioSource = nullptr; disableSoundProcessing = true; audioSyncEnabled = 2; // force udp sound receive mode @@ -1456,19 +1464,25 @@ class AudioReactive : public Usermod { } delay(250); // give microphone enough time to initialise - if (!audioSource && (dmType != 254)) enabled = false;// audio failed to initialise + if (!audioSource && (dmType != SR_DMTYPE_NETWORK_ONLY)) enabled = false;// audio failed to initialise #endif if (enabled) onUpdateBegin(false); // create FFT task, and initialize network #ifdef ARDUINO_ARCH_ESP32 - if (FFT_Task == nullptr) enabled = false; // FFT task creation failed + if (audioSource && FFT_Task == nullptr) enabled = false; // FFT task creation failed if((!audioSource) || (!audioSource->isInitialized())) { // audio source failed to initialize. Still stay "enabled", as there might be input arriving via UDP Sound Sync - #ifdef WLED_DEBUG - DEBUG_PRINTLN(F("AR: Failed to initialize sound input driver. Please check input PIN settings.")); - #else - DEBUGSR_PRINTLN(F("AR: Failed to initialize sound input driver. Please check input PIN settings.")); - #endif +#ifdef WLED_DEBUG + #define AR_INIT_DEBUG_PRINT DEBUG_PRINTLN +#else + #define AR_INIT_DEBUG_PRINT DEBUGSR_PRINTLN +#endif + if (dmType == SR_DMTYPE_NETWORK_ONLY) { + AR_INIT_DEBUG_PRINT(F("AR: No sound input driver configured - network receive only.")); + } else { + AR_INIT_DEBUG_PRINT(F("AR: Failed to initialize sound input driver. Please check input PIN settings.")); + } + #undef AR_INIT_DEBUG_PRINT disableSoundProcessing = true; } #endif @@ -1951,14 +1965,10 @@ class AudioReactive : public Usermod { } #endif } - if (palettes > 0 && root.containsKey(F("rmcpal"))) { - // handle removal of custom palettes from JSON call so we don't break things - removeAudioPalettes(); - } } void onStateChange(uint8_t callMode) override { - if (initDone && enabled && addPalettes && palettes==0 && customPalettes.size()0) { - customPalettes.pop_back(); - DEBUG_PRINTLN(palettes); - palettes--; - } - DEBUG_PRINT(F("Total # of palettes: ")); DEBUG_PRINTLN(customPalettes.size()); + palettes -= (int8_t)removeUsermodPalettes(_name); + if (palettes < 0) palettes = 0; // safeguard } void AudioReactive::createAudioPalettes(void) { - DEBUG_PRINT(F("Total # of palettes: ")); DEBUG_PRINTLN(customPalettes.size()); if (palettes) return; DEBUG_PRINTLN(F("Adding audio palettes.")); - for (int i=0; i= palettes) lastCustPalette -= palettes; - for (int pal=0; pal 1) { //allocate segment data - unsigned dataSize = sizeof(uint32_t) + max(1, (int)SEGLEN -1) *3; //max. 1365 pixels (ESP8266) + unsigned dataSize = sizeof(uint32_t) + max(1, (int)SEGLEN -1) *3; if (!SEGENV.allocateData(dataSize)) candle(false); //allocation failed + } else { + unsigned dataSize = sizeof(uint32_t); // for last call timestamp + if (!SEGENV.allocateData(dataSize)) FX_FALLBACK_STATIC; //allocation failed } uint32_t* lastcall = reinterpret_cast(SEGENV.data); - uint8_t* candleData = reinterpret_cast(SEGENV.data + sizeof(uint32_t)); + uint8_t* candleData = reinterpret_cast(SEGENV.data + sizeof(uint32_t)); // only used for multi-candle //limit update rate if (strip.now - *lastcall < FRAMETIME_FIXED) return; @@ -4425,7 +4430,8 @@ void mode_flow(void) if (zones & 0x01) zones++; //zones must be even if (zones < 2) zones = 2; int zoneLen = SEGLEN / zones; - zones += 2; //add two extra zones to cover beginning and end of segment (compensate integer truncation) + int requiredZones = (SEGLEN + zoneLen - 1) / zoneLen; + zones = requiredZones + 2; //add extra zones to cover beginning and end of segment (compensate integer truncation) int offset = ((int)SEGLEN - (zones * zoneLen)) / 2; // center the zones on the segment (can not use bit shift on negative number) for (int z = 0; z < zones; z++) @@ -5447,6 +5453,7 @@ void mode_2Dgameoflife(void) { // Written by Ewoud Wijma, inspired by https://na } } } + return; } // Repeat detection @@ -8726,7 +8733,7 @@ void mode_particleperlin(void) { PartSys->update(); // update and render } -static const char _data_FX_MODE_PARTICLEPERLIN[] PROGMEM = "PS Fuzzy Noise@Speed,Particles,Bounce,Friction,Scale,Cylinder,Smear,Collide;;!;2;pal=64,sx=50,ix=200,c1=130,c2=30,c3=5,o3=1"; +static const char _data_FX_MODE_PARTICLEPERLIN[] PROGMEM = "PS Fuzzy Noise@Speed,Particles,Bounce,Friction,Scale,Cylinder,Smear,Collide;;!;2;pal=64,sx=50,ix=200,c1=130,c2=30,c3=5"; /* Particle smashing down like meteors and exploding as they hit the ground, has many parameters to play with @@ -9832,6 +9839,7 @@ void mode_particleFireworks1D(void) { PartSys->setColorByPosition(false); // disable for (uint32_t e = 0; e < explosionsize; e++) { // emit explosion particles int idx = PartSys->sprayEmit(PartSys->sources[0]); // emit a particle + if (idx < 0) break; // no more particles available if(SEGMENT.custom3 > 23) { if(SEGMENT.custom3 == 31) { // highest slider value PartSys->setColorByAge(SEGMENT.check1); // color by age if colorful mode is enabled @@ -9856,7 +9864,7 @@ void mode_particleFireworks1D(void) { PartSys->applyFriction(1); // apply friction to all particles PartSys->update(); // update and render - + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { if (PartSys->particles[i].ttl > 20) PartSys->particles[i].ttl -= 20; //ttl is linked to brightness, this allows to use higher brightness but still a short spark lifespan else PartSys->particles[i].ttl = 0; @@ -9906,7 +9914,7 @@ void mode_particleSparkler(void) { PartSys->sources[i].source.ttl = 400; // replenish its life (setting it perpetual uses more code) PartSys->sources[i].sat = SEGMENT.custom1; // color saturation if (SEGMENT.speed == 255) // random position at highest speed setting - PartSys->sources[i].source.x = hw_random16(PartSys->maxX); + PartSys->sources[i].source.x = hw_random(PartSys->maxX); else PartSys->particleMoveUpdate(PartSys->sources[i].source, PartSys->sources[i].sourceFlags, &sparklersettings); //move sparkler } diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index e538b342f6..9f735990b2 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -228,12 +228,19 @@ void Segment::resetIfRequired() { void Segment::loadPalette(CRGBPalette16 &targetPalette, uint8_t pal) { // there is one randomly generated palette (1) followed by 4 palettes created from segment colors (2-5) // those are followed by 7 fastled palettes (6-12) and 59 gradient palettes (13-71) - // then come the custom palettes (255,254,...) growing downwards from 255 (255 being 1st custom palette) + // then come user custom palettes (IDs <=200) and usermod palettes (IDs 201-255), both growing downward from their respective base IDs // palette 0 is a varying palette depending on effect and may be replaced by segment's color if so // instructed in color_from_palette() - if (pal >= FIXED_PALETTE_COUNT && pal <= 255-customPalettes.size()) pal = 0; // out of bounds palette - //default palette. Differs depending on effect - if (pal == 0) pal = _default_palette; // _default_palette is set in setMode() + if (pal == 0) pal = _default_palette; // _default_palette is set in setMode(), differs depending on effect + const int umCount = usermodPalettes.size(); + const int custCount = customPalettes.size(); + if (pal >= FIXED_PALETTE_COUNT) { + if (pal > WLED_CUSTOM_PALETTE_ID_BASE) { // usermod range (IDs 201-255) + if ((WLED_USERMOD_PALETTE_ID_BASE - pal) >= umCount) pal = 0; + } else { // custom range + if ((WLED_CUSTOM_PALETTE_ID_BASE - pal) >= custCount) pal = 0; + } + } switch (pal) { case 0: //default palette. Exceptions for specific effects above targetPalette = PartyColors_gc22; @@ -267,8 +274,10 @@ void Segment::loadPalette(CRGBPalette16 &targetPalette, uint8_t pal) { } break;} default: //progmem palettes - if (pal > 255 - customPalettes.size()) { - targetPalette = customPalettes[255-pal]; // we checked bounds above + if (pal > WLED_CUSTOM_PALETTE_ID_BASE) { // usermod palette + targetPalette = usermodPalettes[WLED_USERMOD_PALETTE_ID_BASE - pal].palette; + } else if (pal >= FIXED_PALETTE_COUNT) { // user custom palette + targetPalette = customPalettes[WLED_CUSTOM_PALETTE_ID_BASE - pal]; } else if (pal < DYNAMIC_PALETTE_COUNT + FASTLED_PALETTE_COUNT) { // palette 6 - 12, fastled palettes targetPalette = *fastledPalettes[pal - DYNAMIC_PALETTE_COUNT]; } else { @@ -585,7 +594,13 @@ Segment &Segment::setMode(uint8_t fx, bool loadDefaults) { } Segment &Segment::setPalette(uint8_t pal) { - if (pal <= 255-customPalettes.size() && pal > FIXED_PALETTE_COUNT) pal = 0; // not built in palette or custom palette + if (pal >= FIXED_PALETTE_COUNT) { + if (pal > WLED_CUSTOM_PALETTE_ID_BASE) { // usermod range + if ((WLED_USERMOD_PALETTE_ID_BASE - pal) >= (int)usermodPalettes.size()) pal = 0; + } else { // custom range + if ((WLED_CUSTOM_PALETTE_ID_BASE - pal) >= (int)customPalettes.size()) pal = 0; + } + } if (pal != palette) { //DEBUG_PRINTF_P(PSTR("- Starting palette transition: %d\n"), pal); startTransition(strip.getTransition(), blendingStyle != TRANSITION_FADE); // start transition prior to change (no need to copy segment) diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index 6615a2e588..175d49ba9c 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -858,6 +858,14 @@ BusHub75Matrix::BusHub75Matrix(const BusConfig &bc) : Bus(bc.type, bc.start, bc. DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA - Matrix Portal S3 config"); mxconfig.gpio = { 42, 41, 40, 38, 39, 37, 45, 36, 48, 35, 21, 47, 14, 2 }; +#elif defined(HD_WF2_PINOUT) // Huidu HD-WF2 ESP32-S3 (no PSRAM) + + // https://www.aliexpress.com/item/1005002258734810.html + // https://github.com/mrcodetastic/ESP32-HUB75-MatrixPanel-DMA/issues/433 + DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA - HD-WF2 S3 config"); + // HUB75_I2S_CFG::i2s_pins _pins={R1_PIN, G1_PIN, B1_PIN, R2_PIN, G2_PIN, B2_PIN, A_PIN, B_PIN, C_PIN, D_PIN, E_PIN, LAT_PIN, OE_PIN, CLK_PIN}; + mxconfig.gpio = { 2, 6, 10, 3, 7, 11, 39, 38, 37, 36, 21, 33, 35, 34 }; + #elif defined(CONFIG_IDF_TARGET_ESP32S3) && defined(BOARD_HAS_PSRAM)// ESP32-S3 with PSRAM #if defined(MOONHUB_S3_PINOUT) @@ -1078,6 +1086,7 @@ uint32_t BusHub75Matrix::getPixelColor(unsigned pix) const { void BusHub75Matrix::setBrightness(uint8_t b) { _bri = b; + if (!_valid || !display) return; display->setBrightness(_bri); } diff --git a/wled00/bus_manager.h b/wled00/bus_manager.h index a71600b44d..abfb08c81b 100644 --- a/wled00/bus_manager.h +++ b/wled00/bus_manager.h @@ -6,7 +6,7 @@ #include #include -#include +#include "src/dependencies/fastled_slim/fastled_slim.h" #endif /* @@ -171,7 +171,7 @@ class Bus { inline bool containsPixel(uint16_t pix) const { return pix >= _start && pix < _start + _len; } static inline std::vector getLEDTypes() { return {{TYPE_NONE, "", PSTR("None")}}; } // not used. just for reference for derived classes - static constexpr size_t getNumberOfPins(uint8_t type) { return isVirtual(type) ? 4 : isPWM(type) ? numPWMPins(type) : isHub75(type) ? 5 : is2Pin(type) + 1; } // credit @PaoloTK + static constexpr size_t getNumberOfPins(uint8_t type) { return isVirtual(type) ? 4 : isPWM(type) ? numPWMPins(type) : isHub75(type) ? 5 : is2Pin(type) + 1; } // credit @PaoloTK; for HUB75 the 5 slots store config params (panelW, panelH, chain, rows, cols), not GPIO pins static constexpr size_t getNumberOfChannels(uint8_t type) { return hasWhite(type) + 3*hasRGB(type) + hasCCT(type); } static constexpr bool hasRGB(uint8_t type) { return !((type >= TYPE_WS2812_1CH && type <= TYPE_WS2812_WWA) || type == TYPE_ANALOG_1CH || type == TYPE_ANALOG_2CH || type == TYPE_ONOFF); diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index d01f83ad54..2e458e7da9 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -30,8 +30,10 @@ static constexpr bool validatePinsAndTypes(const unsigned* types, unsigned numTy // Pins provided < pins required -> always invalid // Pins provided = pins required -> always valid // Pins provided > pins required -> valid if excess pins are a product of last type pins since it will be repeated - return (sumPinsRequired(types, numTypes) > numPins) ? false : - (numPins - sumPinsRequired(types, numTypes)) % Bus::getNumberOfPins(types[numTypes-1]) == 0; + // HUB75 types use their pin slots for config params, not GPIO - skip GPIO pin validation for them + return Bus::isHub75(types[numTypes-1]) ? true : + (sumPinsRequired(types, numTypes) > numPins) ? false : + (numPins - sumPinsRequired(types, numTypes)) % Bus::getNumberOfPins(types[numTypes-1]) == 0; } @@ -47,7 +49,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { //int rev_major = doc["rev"][0]; // 1 //int rev_minor = doc["rev"][1]; // 0 - //long vid = doc[F("vid")]; // 2010020 + long vid = doc[F("vid")] | VERSION; // 2605010 note: "vid" can be used to detect an update from older versions but only on first call, it is written to the new VID after buses are initialized JsonObject id = doc["id"]; getStringFromJson(cmDNS, id[F("mdns")], 33); @@ -693,8 +695,18 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { JsonArray timersArray = tm["ins"]; if (!timersArray.isNull()) { clearTimers(); + bool legacySunriseLoaded = false; // migration flag: pre 16.0 used hour=255 for both sunrise & sunset (type determined by array position) for (JsonObject timer : timersArray) { uint8_t h = timer[F("hour")] | 0; + // legacy migration for pre 16.0 (vid < 2605010): first occurrence = sunrise, second occurrence = sunset + if (vid < 2605010 && h == 255) { + if (legacySunriseLoaded) { + h = TH_SUNSET; // second "255" entry is actually sunset + } else { + legacySunriseLoaded = true; + } + } + int8_t m = timer[F("min")] | 0; uint8_t p = timer[F("macro")] | 0; uint8_t dow = timer[F("dow")] | 127; diff --git a/wled00/colors.cpp b/wled00/colors.cpp index dc15b24676..6ddc4ec892 100644 --- a/wled00/colors.cpp +++ b/wled00/colors.cpp @@ -269,7 +269,10 @@ void loadCustomPalettes() { char fileName[32]; sprintf_P(fileName, PSTR("/palette%d.json"), index); if (WLED_FS.exists(fileName)) { - emptyPaletteGap = 0; // reset gap counter if file exists + // add gray placeholders to preserve palette IDs for subsequent slots (is omitted in UI but shown in cpal.htm) + for (unsigned g = 0; g < emptyPaletteGap; g++) + customPalettes.push_back(CRGBPalette16(CRGB(128, 128, 128))); + emptyPaletteGap = 0; // reset gap counter if file exists DEBUGFX_PRINTF_P(PSTR("Reading palette from %s\n"), fileName); if (readObjectFromFile(fileName, nullptr, &pDoc)) { JsonArray pal = pDoc[F("palette")]; @@ -309,6 +312,15 @@ void loadCustomPalettes() { } } +size_t removeUsermodPalettes(const char *name) { + size_t before = usermodPalettes.size(); + for (int i = usermodPalettes.size() - 1; i >= 0; i--) { + if (usermodPalettes[i].name == name) + usermodPalettes.erase(usermodPalettes.begin() + i); + } + return before - usermodPalettes.size(); +} + // convert HSV (16bit hue) to RGB (32bit with white = 0), optimized for speed WLED_O2_ATTR void hsv2rgb_spectrum(const CHSV32& hsv, CRGBW& rgb) { unsigned p, q, t; diff --git a/wled00/colors.h b/wled00/colors.h index 7b8c791c47..105048d35d 100644 --- a/wled00/colors.h +++ b/wled00/colors.h @@ -61,9 +61,20 @@ void adjust_color(CRGBW& rgb, int32_t hueShift, int32_t satChange,int32_t valueC [[gnu::hot, gnu::pure]] uint32_t ColorFromPalette(const CRGBPalette16 &pal, unsigned index, uint8_t brightness = (uint8_t)255U, TBlendType blendType = LINEARBLEND); CRGBPalette16 generateHarmonicRandomPalette(const CRGBPalette16 &basepalette); CRGBPalette16 generateRandomPalette(); +// Palette registered by a usermod at fixed IDs (255, 254, 253... 201). +// Display name is "name: palName" (if palName non-null) or falls back to "name index" (e.g. "AudioReactive 1"), see util.cpp +struct UsermodPalette { + CRGBPalette16 palette; + const char *name; // PROGMEM base name string (must not be nullptr), this name is used in removeUsermodPalettes() + uint8_t palIndex; // index of the palette for a usermod + const char *palName; // optional PROGMEM display name; if set, shown as "name: palName" (e.g. "AudioReactive: Audio Responsive Hue"), otherwise falls back to "name index" +}; + void loadCustomPalettes(); +size_t removeUsermodPalettes(const char *name); // remove all entries from usermodPalettes whose name pointer matches 'name' extern std::vector customPalettes; -inline size_t getPaletteCount() { return FIXED_PALETTE_COUNT + customPalettes.size(); } +extern std::vector usermodPalettes; +inline size_t getPaletteCount() { return FIXED_PALETTE_COUNT + usermodPalettes.size() + customPalettes.size(); } void hsv2rgb_spectrum(const CHSV32& hsv, CRGBW& rgb); void hsv2rgb_spectrum(const CHSV& hsv, CRGB& rgb); diff --git a/wled00/const.h b/wled00/const.h index e49dd2900a..62d9c45f4d 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -10,8 +10,16 @@ constexpr size_t FASTLED_PALETTE_COUNT = 7; // 6-12 = sizeof(fastledPalettes) constexpr size_t GRADIENT_PALETTE_COUNT = 59; // 13-72 = sizeof(gGradientPalettes) / sizeof(gGradientPalettes[0]); constexpr size_t DYNAMIC_PALETTE_COUNT = 6; // 0- 5 = dynamic palettes (0=default(virtual),1=random,2=primary,3=primary+secondary,4=primary+secondary+tertiary,5=primary+secondary(+tertiary if not black) constexpr size_t FIXED_PALETTE_COUNT = DYNAMIC_PALETTE_COUNT + FASTLED_PALETTE_COUNT + GRADIENT_PALETTE_COUNT; // total number of fixed palettes + +// Palette ID space layout (palette IDs are uint8_t, 0-255): +// 0 .. FIXED_PALETTE_COUNT-1 : fixed built-in palettes +// 72 .. WLED_CUSTOM_PALETTE_ID_BASE(200) : user custom palettes (index 0 = ID 200, growing downward) +// 201.. WLED_USERMOD_PALETTE_ID_BASE(255): usermod-registered palettes (index 0 = ID 255, growing downward) +constexpr uint8_t WLED_USERMOD_PALETTE_ID_BASE = 255; // highest ID for usermod palettes +constexpr uint8_t WLED_CUSTOM_PALETTE_ID_BASE = 200; // highest ID for user custom palettes +constexpr size_t WLED_MAX_USERMOD_PALETTES = WLED_USERMOD_PALETTE_ID_BASE - WLED_CUSTOM_PALETTE_ID_BASE; // 55 slots (IDs 201-255) #ifndef ESP8266 - #define WLED_MAX_CUSTOM_PALETTES (255 - FIXED_PALETTE_COUNT) // allow up to 255 total palettes, user is warned about stability issues when adding more than 10 + #define WLED_MAX_CUSTOM_PALETTES (WLED_CUSTOM_PALETTE_ID_BASE - FIXED_PALETTE_COUNT + 1) // 129 slots (IDs 72-200) #else #define WLED_MAX_CUSTOM_PALETTES 10 // ESP8266: limit custom palettes to 10 #endif @@ -382,7 +390,7 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit"); #define BTN_TYPE_TOUCH_SWITCH 9 //Ethernet board types -#define WLED_NUM_ETH_TYPES 14 +#define WLED_NUM_ETH_TYPES 16 #define WLED_ETH_NONE 0 @@ -399,6 +407,9 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit"); #define WLED_ETH_ESP32_POE_WROVER 11 #define WLED_ETH_LILYGO_T_POE_PRO 12 #define WLED_ETH_GLEDOPTO 13 +#define WLED_ETH_QUINLED_V4_UNOQUAD 14 +#define WLED_ETH_QUINLED_V4_OCTA 15 + //Hue error codes #define HUE_ERROR_INACTIVE 0 @@ -481,6 +492,7 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit"); #define JSON_LOCK_LEDGAP 20 #define JSON_LOCK_LEDMAP_ENUM 21 #define JSON_LOCK_REMOTE 22 +#define JSON_LOCK_OTA 23 // Timer mode types #define NL_MODE_SET 0 //After nightlight time elapsed, set to target brightness diff --git a/wled00/data/common.js b/wled00/data/common.js index 03103798c2..61a6e4bd05 100644 --- a/wled00/data/common.js +++ b/wled00/data/common.js @@ -200,7 +200,7 @@ function sendDDP(ws, start, len, colors) { let pkt = new Uint8Array(11 + dLen); // DDP header is 10 bytes, plus 1 byte for WLED websocket protocol indicator pkt[0] = 0x02; // DDP protocol indicator for WLED websocket. Note: below DDP protocol bytes are offset by 1 pkt[1] = 0x40; // flags: 0x40 = no push, 0x41 = push (i.e. render), note: this is DDP protocol byte 0 - pkt[2] = 0x00; // reserved + pkt[2] = 0x00; // upper nibble is reserved, lower nibble is sequence number, if set to 0 no sequence checking is done (if enabled) pkt[3] = 0x0B; // RGB, 8bit per channel pkt[4] = 0x01; // destination id (not used but 0x01 is default output) pkt[5] = (off >> 24) & 255; // DDP protocol 4-7 is offset @@ -222,3 +222,138 @@ function sendDDP(ws, start, len, colors) { } return true; } + +// Pin utilities +function getOwnerName(o,t,n) { + // Use firmware-provided name if available + if(n) return n; + if(!o) return "System"; // no owner provided + if(o===0x85){ return getBtnTypeName(t); } // button pin + return "UM #"+o; +} +function getBtnTypeName(t) { + var n=["None","Reserved","Push","Push Inv","Switch","PIR","Touch","Analog","Analog Inv","Touch Switch"]; + var label = n[t] || "?"; + return 'Button '+label+''; +} +function getCaps(p,c) { + var r=[]; + // Use touch info from settings endpoint + if(d.touch && d.touch.includes(p)) r.push("Touch"); + if(d.ro_gpio && d.ro_gpio.includes(p)) r.push("Input Only"); + // Use other caps from JSON (Analog, Boot, Input Only) + if(c&0x02) r.push("Analog"); + if(c&0x08) r.push("Flash Boot"); + if(c&0x10) r.push("Bootstrap"); + return r.length?r.join(", "):"-"; +} + +// Fetch GPIO caps (/settings/s.js?p=11) then pin occupancy (/json/pins) with retry. +// Caches result in d.pinsData. Calls cb() when ready (or on failure). +// If page already loaded its own s.js (d.max_gpio set), skips caps load and goes straight to pins fetch. +function fetchPinInfo(cb, retries=5) { + if (d.pinsData) { cb&&cb(); return; } + var done=false, fr=retries; + function doFetch() { + fetch(getURL('/json/pins')) + .then(r=>r.json()) + .then(j=>{ if(!done){done=true; d.pinsData=j.pins||[]; cb&&cb();} }) + .catch(()=>{ fr-->0 ? setTimeout(doFetch,100) : (!done&&(done=true,d.pinsData=[],cb&&cb())); }); + } + if (d.max_gpio) { doFetch(); return; } + // Load GPIO caps from s.js?p=11 first (sets d.rsvd/ro_gpio/max_gpio/touch/adc/um_p) + d.max_gpio=50; d.rsvd=[]; d.ro_gpio=[]; d.touch=[]; d.adc=[]; d.um_p=[]; + var cr=retries; + function tryCaps() { + var s=cE("script"); s.src=getURL('/settings/s.js?p=11'); + d.body.appendChild(s); + s.onload=function(){ GetV(); doFetch(); }; + s.onerror=function(){ cr-->0 ? setTimeout(tryCaps,100) : doFetch(); }; + } + tryCaps(); +} + +// Pin dropdown utilities +// Create or rebuild a pin or existing back to +function unmakePinSelect(name) { + let sel = gN(name); + if (!sel || sel.tagName !== "SELECT") return null; + let inp = cE('input'); + inp.type = "number"; + inp.name = sel.name; + inp.value = sel.value; + inp.className = "s"; + if (sel.required) inp.required = true; + let oc = sel.getAttribute("onchange"); + if (oc) inp.setAttribute("onchange", oc); + sel.parentElement.replaceChild(inp, sel); + return inp; +} +// Add option to select, auto-select matching data-val +function addOption(sel, txt, val) { + if (!sel) return null; + let opt = cE("option"); + opt.value = val; + opt.text = txt; + sel.appendChild(opt); + if (sel.dataset.val !== undefined) { + for (let i = 0; i < sel.options.length; i++) { + if (sel.options[i].value == sel.dataset.val) { sel.selectedIndex = i; break; } + } + } + return opt; +} diff --git a/wled00/data/cpal/cpal.htm b/wled00/data/cpal/cpal.htm index 4e41757a94..841c95be32 100644 --- a/wled00/data/cpal/cpal.htm +++ b/wled00/data/cpal/cpal.htm @@ -5,7 +5,7 @@ WLED Palette Editor @@ -71,6 +71,8 @@ let copyColor = '#000'; let ws = null; let maxCol; // max colors to send out in one chunk, ESP8266 is limited to ~50 (500 bytes), ESP32 can do ~128 (1340 bytes) + let _applySeq = 0; // incremented each time applyLED fires; used to cancel stale in-flight previews + let _httpQueue = [], _httpRun = 0; // load external resources in sequence to avoid 503 errors if heap is low, repeats indefinitely until loaded (function loadFiles() { @@ -329,17 +331,7 @@ }); draw(); } -/* - function rndPal() { - gr.innerHTML = ''; - addMk(0, rndHex(), 1); - const cnt = Math.floor(Math.random() * 3) + 2, pos = new Set(); - while (pos.size < cnt) pos.add(Math.floor(Math.random() * 254) + 1); - [...pos].sort((a,b)=>a-b).forEach(t => addMk(t, rndHex())); - addMk(255, rndHex(), 1); - }*/ - // convert hsl to hex using canvas function hslToHex(h, s, l) { let ctx = cE("canvas").getContext("2d"); @@ -468,7 +460,11 @@ rm.className = 'sml'; rm.title = 'Delete palette'; rm.innerHTML = '✖'; - rm.onclick = () => { requestJson({rmcpal:i}); setTimeout(refr, 500); }; + rm.onclick = () => { + requestJson({rmcpal:i}); // send remove command + setTimeout(refr, 500); // slight delay to allow ESP to process deletion before fetching updated list + localStorage.removeItem('wledPalx'); // invalidate main UI cache + }; const name = isEmpty(p.palette) ? 'Empty slot' : 'Custom' + i; const css = isEmpty(p.palette) ? '#666' : cssArr(p.palette); @@ -592,7 +588,7 @@ // download external palettes, these were hand picked from cpt-city (http://seaviewsensing.com/pub/cpt-city/) // all palettes are licensed "free to use", converted to WLED JSON format by @dedehai function fetchExt() { - fetch('https://dedehai.github.io/cpt_city_selection.json') + fetch('https://wled.github.io/wled-web-tools/cpt_city_selection.json') .then(r => { if (!r.ok) throw new Error(); return r.json(); }) .then(data => { try { localStorage.setItem('wledCptCityJson', JSON.stringify(data)); } catch(e) {} @@ -621,21 +617,16 @@ async function requestJson(cmd) { - if (ws && ws.readyState == 1) { + if (ws && ws.readyState == 1 && ws.bufferedAmount < 32768) { try { ws.send(JSON.stringify(cmd)); + await new Promise(r => setTimeout(r, 15)); // short delay to give ESP time to process (fewer packets dropped) return 1; } catch (e) {} } - if (!window._httpQueue) { - window._httpQueue = []; - window._httpRun = 0; - } - if (_httpQueue.length >= 5) { - return Promise.resolve(-1); // reject if too many queued requests - } - + // HTTP fallback + if (_httpQueue.length >= 5) return -1; // queue full; applyLED cancels stale queues before sending return new Promise(resolve => { _httpQueue.push({ cmd, resolve }); (async function run() { @@ -650,7 +641,7 @@ cache: 'no-store' }); } catch (e) {} - await new Promise(r => setTimeout(r, 120)); + await new Promise(r => setTimeout(r, 120)); // delay between requests (go slow, this is the http fallback if WS fails) q.resolve(0); } _httpRun = 0; @@ -662,8 +653,12 @@ async function applyLED() { if (!palCache.length) return; + const seq = ++_applySeq; + // discard pending HTTP chunks from any previous preview so stale data doesn't drain slowly + while (_httpQueue.length) _httpQueue.shift().resolve(-1); // resolve dropped entries so their awaiters can observe the seq change and exit try { let st = await (await fetch(getURL('/json/state'), { cache: 'no-store' })).json(); + if (seq !== _applySeq) return; // superseded by a newer preview request if (!st.seg || !st.seg.length) return; // get selected segments, use main segment if none selected @@ -680,6 +675,7 @@ arr.push(palCache[len > 1 ? Math.round(i * 255 / (len - 1)) : 0]); // send colors in chunks for (let j = 0; j < arr.length; j += maxCol) { + if (seq !== _applySeq) return; // superseded mid-send let chunk = [s.start + j, ...arr.slice(j, j + maxCol)]; await requestJson({ seg: { id: s.id, i: chunk } }); } diff --git a/wled00/data/icons-ui/demo-files/demo.css b/wled00/data/icons-ui/demo-files/demo.css index 731f7b29b6..8fd83c7923 100644 --- a/wled00/data/icons-ui/demo-files/demo.css +++ b/wled00/data/icons-ui/demo-files/demo.css @@ -147,7 +147,7 @@ p { font-size: 16px; } .fs1 { - font-size: 48px; + font-size: 32px; } .fs2 { font-size: 28px; diff --git a/wled00/data/icons-ui/demo.html b/wled00/data/icons-ui/demo.html index c5d596e966..a88b956bbd 100644 --- a/wled00/data/icons-ui/demo.html +++ b/wled00/data/icons-ui/demo.html @@ -9,10 +9,24 @@
-

Font Name: wled122 (Glyphs: 25)

+

Font Name: wled122 (Glyphs: 26)

Grid Size: 16

+
+
+ + i-search +
+
+ + +
+
+ liga: + +
+
diff --git a/wled00/data/icons-ui/fonts/wled122.svg b/wled00/data/icons-ui/fonts/wled122.svg index 082edf54d3..1c0b8ea0b3 100644 --- a/wled00/data/icons-ui/fonts/wled122.svg +++ b/wled00/data/icons-ui/fonts/wled122.svg @@ -11,6 +11,7 @@ + diff --git a/wled00/data/icons-ui/fonts/wled122.ttf b/wled00/data/icons-ui/fonts/wled122.ttf index 84f7719585..4f238de0c7 100644 Binary files a/wled00/data/icons-ui/fonts/wled122.ttf and b/wled00/data/icons-ui/fonts/wled122.ttf differ diff --git a/wled00/data/icons-ui/fonts/wled122.woff b/wled00/data/icons-ui/fonts/wled122.woff index 2fdda4df7d..abbeea0f48 100644 Binary files a/wled00/data/icons-ui/fonts/wled122.woff and b/wled00/data/icons-ui/fonts/wled122.woff differ diff --git a/wled00/data/icons-ui/fonts/wled122.woff2 b/wled00/data/icons-ui/fonts/wled122.woff2 index f049a31aff..7fd5f567e3 100644 Binary files a/wled00/data/icons-ui/fonts/wled122.woff2 and b/wled00/data/icons-ui/fonts/wled122.woff2 differ diff --git a/wled00/data/icons-ui/selection.json b/wled00/data/icons-ui/selection.json index b2c2b14a91..f516914d8e 100644 --- a/wled00/data/icons-ui/selection.json +++ b/wled00/data/icons-ui/selection.json @@ -1 +1 @@ -{"IcoMoonType":"selection","icons":[{"icon":{"paths":["M910.398 765.581l-241.236-241.236c-14.934-14.934-39.371-14.934-54.306 0l-18.102 18.102-147.2-147.2 241.646-241.648h-256.001l-113.645 113.645-11.249-11.247h-54.306v54.306l11.247 11.247-164.848 164.849 127.999 127.999 164.848-164.848 147.2 147.2-18.102 18.102c-14.934 14.934-14.934 39.371 0 54.306l241.236 241.236c14.934 14.934 39.371 14.934 54.306 0l90.509-90.509c14.935-14.934 14.935-39.371 0.002-54.306z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"tags":["hammer","tool","fix","make","generate","work","build"],"grid":16},"attrs":[{}],"properties":{"order":1,"id":0,"name":"pixelforge","prevSize":48,"code":59648},"setIdx":0,"setId":3,"iconIdx":0},{"icon":{"paths":["M976.272 538.191c0 11.223-7.016 22.448-14.5 30.867l-157.14 185.202c-27.126 31.802-82.311 57.055-123.469 57.055h-508.837c-16.837 0-40.688-5.146-40.688-26.191 0-11.223 7.016-22.448 14.5-30.867l157.14-185.202c27.126-31.802 82.311-57.055 123.469-57.055h508.837c16.837 0 40.688 5.146 40.688 26.191zM815.856 377.307v74.828h-389.112c-58.461 0-130.952 33.208-168.835 78.104l-159.949 188.009c0-3.74-0.467-7.951-0.467-11.691v-448.977c0-57.523 47.233-104.761 104.761-104.761h149.66c57.523 0 104.761 47.233 104.761 104.761v14.968h254.418c57.523 0 104.761 47.233 104.761 104.761z"],"attrs":[{}],"width":1074,"isMulticolor":false,"isMulticolor2":false,"tags":["folder-open"],"grid":14},"attrs":[{}],"properties":{"order":1,"id":1,"prevSize":28,"name":"editor","code":59649},"setIdx":1,"setId":2,"iconIdx":0},{"icon":{"paths":["M511.573 85.333c235.947 0 427.094 191.147 427.094 426.667s-191.147 426.667-427.094 426.667c-235.52 0-426.24-191.147-426.24-426.667s190.72-426.667 426.24-426.667zM512 853.333c188.587 0 341.333-152.746 341.333-341.333s-152.746-341.333-341.333-341.333-341.333 152.746-341.333 341.333 152.746 341.333 341.333 341.333zM661.333 469.333c-35.413 0-64-28.586-64-64s28.587-64 64-64c35.414 0 64 28.587 64 64s-28.586 64-64 64zM362.667 469.333c-35.414 0-64-28.586-64-64s28.586-64 64-64c35.413 0 64 28.587 64 64s-28.587 64-64 64zM512 746.667c-99.413 0-183.893-62.294-218.027-149.334h436.054c-34.134 87.040-118.614 149.334-218.027 149.334z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE23D"],"defaultCode":57917,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":11,"order":26,"prevSize":32,"code":57917,"name":"pattern"},"setIdx":2,"setId":1,"iconIdx":0},{"icon":{"paths":["M511.573 791.040l314.88-244.907 69.547 54.187-384 298.667-384-298.667 69.12-53.76zM512 682.667l-384-298.667 384-298.667 384 298.667-69.973 54.187z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE34B"],"defaultCode":58187,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":14,"order":35,"ligatures":"","prevSize":32,"code":58187,"name":"segments"},"setIdx":2,"setId":1,"iconIdx":1},{"icon":{"paths":["M288.427 206.507l-60.587 60.16-76.373-76.374 60.16-60.16zM170.667 448v85.333h-128v-85.333h128zM554.667 23.467v125.866h-85.334v-125.866h85.334zM872.533 190.293l-76.373 76.374-60.16-60.16 76.373-76.374zM735.573 774.827l59.734-59.734 76.8 76.374-60.16 60.16zM853.333 448h128v85.333h-128v-85.333zM512 234.667c141.227 0 256 114.773 256 256 0 141.226-114.773 256-256 256s-256-114.774-256-256c0-141.227 114.773-256 256-256zM469.333 957.867v-125.867h85.334v125.867h-85.334zM151.467 791.040l76.373-76.8 60.16 60.16-76.373 76.8z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE333"],"defaultCode":58163,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":40,"order":73,"ligatures":"","prevSize":32,"code":58163,"name":"sun"},"setIdx":2,"setId":1,"iconIdx":2},{"icon":{"paths":["M512 128c212.053 0 384 152.747 384 341.333 0 117.76-95.573 213.334-213.333 213.334h-75.52c-35.414 0-64 28.586-64 64 0 16.213 6.4 31.146 16.213 42.24 10.24 11.52 16.64 26.453 16.64 43.093 0 35.413-28.587 64-64 64-212.053 0-384-171.947-384-384s171.947-384 384-384zM277.333 512c35.414 0 64-28.587 64-64s-28.586-64-64-64c-35.413 0-64 28.587-64 64s28.587 64 64 64zM405.333 341.333c35.414 0 64-28.586 64-64 0-35.413-28.586-64-64-64-35.413 0-64 28.587-64 64 0 35.414 28.587 64 64 64zM618.667 341.333c35.413 0 64-28.586 64-64 0-35.413-28.587-64-64-64-35.414 0-64 28.587-64 64 0 35.414 28.586 64 64 64zM746.667 512c35.413 0 64-28.587 64-64s-28.587-64-64-64c-35.414 0-64 28.587-64 64s28.586 64 64 64z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE2B3"],"defaultCode":58035,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":75,"order":75,"prevSize":32,"code":58035,"name":"palette"},"setIdx":2,"setId":1,"iconIdx":3},{"icon":{"paths":["M512 192c213.333 0 395.52 132.693 469.333 320-73.813 187.307-256 320-469.333 320s-395.52-132.693-469.333-320c73.813-187.307 256-320 469.333-320zM512 725.333c117.76 0 213.333-95.573 213.333-213.333s-95.573-213.333-213.333-213.333-213.333 95.573-213.333 213.333 95.573 213.333 213.333 213.333zM512 384c70.827 0 128 57.173 128 128s-57.173 128-128 128-128-57.173-128-128 57.173-128 128-128z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE0E8"],"defaultCode":57576,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":172,"order":74,"prevSize":32,"code":57576,"name":"eye"},"setIdx":2,"setId":1,"iconIdx":4},{"icon":{"paths":["M640 42.667v85.333h-256v-85.333h256zM469.333 597.333v-256h85.334v256h-85.334zM811.947 315.307c52.48 65.706 84.053 148.906 84.053 239.36 0 212.053-171.52 384-384 384s-384-171.947-384-384c0-212.054 171.947-384 384-384 90.453 0 173.653 31.573 239.787 84.48l60.586-60.587c21.76 17.92 41.814 38.4 60.16 60.16zM512 853.333c165.12 0 298.667-133.546 298.667-298.666s-133.547-298.667-298.667-298.667-298.667 133.547-298.667 298.667 133.547 298.666 298.667 298.666z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE325"],"defaultCode":58149,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":370,"order":21,"ligatures":"","prevSize":32,"code":58149,"name":"speed"},"setIdx":2,"setId":1,"iconIdx":5},{"icon":{"paths":["M707.84 366.507l60.16 60.16-256 256-256-256 60.16-60.16 195.84 195.413z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE395"],"defaultCode":58261,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":549,"order":69,"ligatures":"","prevSize":32,"code":58261,"name":"expand"},"setIdx":2,"setId":1,"iconIdx":6},{"icon":{"paths":["M554.667 128v426.667h-85.334v-426.667h85.334zM760.747 220.587c82.773 70.4 135.253 174.506 135.253 291.413 0 212.053-171.947 384-384 384s-384-171.947-384-384c0-116.907 52.48-221.013 135.253-291.413l60.16 60.16c-66.986 54.613-110.080 137.813-110.080 231.253 0 165.12 133.547 298.667 298.667 298.667s298.667-133.547 298.667-298.667c0-93.44-43.094-176.64-110.507-230.827z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE08F"],"defaultCode":57487,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":557,"order":19,"ligatures":"","prevSize":32,"code":57487,"name":"power"},"setIdx":2,"setId":1,"iconIdx":7},{"icon":{"paths":["M816.64 551.936l85.504 67.584c8.192 6.144 10.24 16.896 5.12 26.112l-81.92 141.824c-5.12 9.216-15.872 12.8-25.088 9.216l-101.888-40.96c-20.992 15.872-44.032 29.696-69.12 39.936l-15.36 108.544c-1.024 10.24-9.728 17.408-19.968 17.408h-163.84c-10.24 0-18.432-7.168-20.48-17.408l-15.36-108.544c-25.088-10.24-47.616-23.552-69.12-39.936l-101.888 40.96c-9.216 3.072-19.968 0-25.088-9.216l-81.92-141.824c-4.608-8.704-2.56-19.968 5.12-26.112l86.528-67.584c-2.048-12.8-3.072-26.624-3.072-39.936s1.536-27.136 3.584-39.936l-86.528-67.584c-8.192-6.144-10.24-16.896-5.12-26.112l81.92-141.824c5.12-9.216 15.872-12.8 25.088-9.216l101.888 40.96c20.992-15.872 44.032-29.696 69.12-39.936l15.36-108.544c1.536-10.24 9.728-17.408 19.968-17.408h163.84c10.24 0 18.944 7.168 20.48 17.408l15.36 108.544c25.088 10.24 47.616 23.552 69.12 39.936l101.888-40.96c9.216-3.072 19.968 0 25.088 9.216l81.92 141.824c4.608 8.704 2.56 19.968-5.12 26.112l-86.528 67.584c2.048 12.8 3.072 26.112 3.072 39.936s-1.024 27.136-2.56 39.936zM512 665.6c84.48 0 153.6-69.12 153.6-153.6s-69.12-153.6-153.6-153.6-153.6 69.12-153.6 153.6 69.12 153.6 153.6 153.6z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE0A2"],"defaultCode":57506,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":562,"order":29,"ligatures":"","prevSize":32,"code":57506,"name":"settings"},"setIdx":2,"setId":1,"iconIdx":8},{"icon":{"paths":["M556.8 417.707l125.867 94.293-256 192v-384zM556.8 417.707l125.867 94.293-256 192v-384zM556.8 417.707l-130.133-97.707v384l256-192zM469.333 173.653c-62.293 7.68-119.040 32.427-166.4 69.12l-60.586-61.013c63.146-51.627 141.226-85.76 226.986-94.293v86.186zM242.773 302.933c-36.693 47.36-61.44 104.107-69.12 166.4h-86.186c8.533-85.76 42.666-163.84 94.293-226.986zM173.653 554.667c7.68 62.293 32.427 119.040 69.12 165.973l-61.013 61.013c-51.627-63.146-85.76-141.226-94.293-226.986h86.186zM242.347 842.24l60.586-61.013c47.36 36.693 104.107 61.44 166.4 69.12v86.186c-85.333-8.533-163.84-42.666-226.986-94.293zM938.667 512c0 220.16-167.254 401.92-381.867 424.533v-86.186c167.253-22.187 296.533-165.547 296.533-338.347s-129.28-316.16-296.533-338.347v-86.186c214.613 22.613 381.867 204.373 381.867 424.533z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE139"],"defaultCode":57657,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":595,"order":46,"ligatures":"","prevSize":32,"code":57657,"name":"playlist"},"setIdx":2,"setId":1,"iconIdx":9},{"icon":{"paths":["M386.4 93.333c231.104 0 418.667 187.563 418.667 418.667s-187.563 418.667-418.667 418.667c-43.96 0-85.827-6.699-125.6-19.259 169.979-53.171 293.067-211.845 293.067-399.408s-123.088-346.237-293.067-399.408c39.773-12.56 81.64-19.259 125.6-19.259z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE2A2"],"defaultCode":58018,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":607,"order":34,"ligatures":"","prevSize":32,"code":58018,"name":"night"},"setIdx":2,"setId":1,"iconIdx":10},{"icon":{"paths":["M512 85.333c235.947 0 426.667 190.72 426.667 426.667s-190.72 426.667-426.667 426.667-426.667-190.72-426.667-426.667 190.72-426.667 426.667-426.667zM725.333 665.173l-153.173-153.173 153.173-153.173-60.16-60.16-153.173 153.173-153.173-153.173-60.16 60.16 153.173 153.173-153.173 153.173 60.16 60.16 153.173-153.173 153.173 153.173z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE38F"],"defaultCode":58255,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":662,"order":50,"ligatures":"","prevSize":32,"code":58255,"name":"cancel"},"setIdx":2,"setId":1,"iconIdx":11},{"icon":{"paths":["M512 170.667c188.587 0 341.333 152.746 341.333 341.333 0 66.987-19.626 129.28-52.906 181.76l-62.294-62.293c19.2-35.414 29.867-76.374 29.867-119.467 0-141.227-114.773-256-256-256v128l-170.667-170.667 170.667-170.666v128zM512 768v-128l170.667 170.667-170.667 170.666v-128c-188.587 0-341.333-152.746-341.333-341.333 0-66.987 19.626-129.28 52.906-181.76l62.294 62.293c-19.2 35.414-29.867 76.374-29.867 119.467 0 141.227 114.773 256 256 256z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE116"],"defaultCode":57622,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":709,"order":17,"ligatures":"","prevSize":32,"code":57622,"name":"sync"},"setIdx":2,"setId":1,"iconIdx":12},{"icon":{"paths":["M384 689.92l451.84-451.413 60.16 60.16-512 512-238.507-238.507 60.587-60.16z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE390"],"defaultCode":58256,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":733,"order":56,"ligatures":"","prevSize":32,"code":58256,"name":"confirm"},"setIdx":2,"setId":1,"iconIdx":13},{"icon":{"paths":["M853.333 370.773l141.227 141.227-141.227 141.227v200.106h-200.106l-141.227 141.227-141.227-141.227h-200.106v-200.106l-141.227-141.227 141.227-141.227v-200.106h200.106l141.227-141.227 141.227 141.227h200.106v200.106zM512 768c141.227 0 256-114.773 256-256s-114.773-256-256-256-256 114.773-256 256 114.773 256 256 256zM512 341.333c94.293 0 170.667 76.374 170.667 170.667s-76.374 170.667-170.667 170.667-170.667-76.374-170.667-170.667 76.374-170.667 170.667-170.667z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE2A6"],"defaultCode":58022,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":785,"order":15,"ligatures":"","prevSize":32,"code":58022,"name":"brightness"},"setIdx":2,"setId":1,"iconIdx":14},{"icon":{"paths":["M85.333 725.333v-42.666h128v170.666h-128v-42.666h85.334v-21.334h-42.667v-42.666h42.667v-21.334h-85.334zM128 341.333v-128h-42.667v-42.666h85.334v170.666h-42.667zM85.333 469.333v-42.666h128v38.4l-76.8 89.6h76.8v42.666h-128v-38.4l76.8-89.6h-76.8zM298.667 213.333h597.333v85.334h-597.333v-85.334zM298.667 810.667v-85.334h597.333v85.334h-597.333zM298.667 554.667v-85.334h597.333v85.334h-597.333z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE22D"],"defaultCode":57901,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":797,"order":58,"ligatures":"","prevSize":32,"code":57901,"name":"nodes"},"setIdx":2,"setId":1,"iconIdx":15},{"icon":{"paths":["M810.667 554.667h-256v256h-85.334v-256h-256v-85.334h256v-256h85.334v256h256v85.334z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE18A"],"defaultCode":57738,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":803,"order":59,"ligatures":"","prevSize":32,"code":57738,"name":"add"},"setIdx":2,"setId":1,"iconIdx":16},{"icon":{"paths":["M128 736l471.893-471.893 160 160-471.893 471.893h-160v-160zM883.627 300.373l-78.080 78.080-160-160 78.080-78.080c16.64-16.64 43.52-16.64 60.16 0l99.84 99.84c16.64 16.64 16.64 43.52 0 60.16z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE2C6"],"defaultCode":58054,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":834,"order":72,"ligatures":"","prevSize":32,"code":58054,"name":"edit"},"setIdx":2,"setId":1,"iconIdx":17},{"icon":{"paths":["M576 28.587c166.827 133.546 277.333 338.773 277.333 568.746 0 188.587-152.746 341.334-341.333 341.334s-341.333-152.747-341.333-341.334c0-144.213 51.626-276.906 137.813-379.306l-1.28 15.36c0 87.893 66.56 159.146 154.88 159.146 87.893 0 145.493-71.253 145.493-159.146 0-91.734-31.573-204.8-31.573-204.8zM499.627 810.667c113.066 0 204.8-91.734 204.8-204.8 0-59.307-8.534-117.334-25.174-172.374-43.52 58.454-121.6 94.72-197.12 110.080-75.093 15.36-119.893 64-119.893 133.12 0 74.24 61.44 133.974 137.387 133.974z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE409"],"defaultCode":58377,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":871,"order":10,"ligatures":"","prevSize":32,"code":58377,"name":"intensity"},"setIdx":2,"setId":1,"iconIdx":18},{"icon":{"paths":["M938.667 394.24l-232.534 201.813 69.547 299.947-263.68-159.147-263.68 159.147 69.973-299.947-232.96-201.813 306.774-26.027 119.893-282.88 119.893 282.454zM512 657.067l160.853 97.28-42.666-182.614 141.653-122.88-186.88-16.213-72.96-172.373-72.533 171.946-186.88 16.214 141.653 122.88-42.667 182.613z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE410"],"defaultCode":58384,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":927,"order":9,"ligatures":"","prevSize":32,"code":58384,"name":"star"},"setIdx":2,"setId":1,"iconIdx":19},{"icon":{"paths":["M512 85.333c235.52 0 426.667 191.147 426.667 426.667s-191.147 426.667-426.667 426.667-426.667-191.147-426.667-426.667 191.147-426.667 426.667-426.667zM554.667 725.333v-256h-85.334v256h85.334zM554.667 384v-85.333h-85.334v85.333h85.334z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE066"],"defaultCode":57446,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":952,"order":62,"ligatures":"","prevSize":32,"code":57446,"name":"info"},"setIdx":2,"setId":1,"iconIdx":20},{"icon":{"paths":["M256 810.667v-512h512v512c0 46.933-38.4 85.333-85.333 85.333h-341.334c-46.933 0-85.333-38.4-85.333-85.333zM810.667 170.667v85.333h-597.334v-85.333h149.334l42.666-42.667h213.334l42.666 42.667h149.334z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE037"],"defaultCode":57399,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":969,"order":55,"ligatures":"","prevSize":32,"code":57399,"name":"del"},"setIdx":2,"setId":1,"iconIdx":21},{"icon":{"paths":["M704 128c131.413 0 234.667 103.253 234.667 234.667 0 161.28-145.067 292.693-364.8 491.946l-61.867 56.32-61.867-55.893c-219.733-199.68-364.8-331.093-364.8-492.373 0-131.414 103.254-234.667 234.667-234.667 74.24 0 145.493 34.56 192 89.173 46.507-54.613 117.76-89.173 192-89.173zM516.267 791.467c203.093-183.894 337.066-305.494 337.066-428.8 0-85.334-64-149.334-149.333-149.334-65.707 0-129.707 42.24-151.893 100.694h-79.787c-22.613-58.454-86.613-100.694-152.32-100.694-85.333 0-149.333 64-149.333 149.334 0 123.306 133.973 244.906 337.066 428.8l4.267 4.266z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE04C"],"defaultCode":57420,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":1034,"order":61,"ligatures":"","prevSize":32,"code":57420,"name":"presets"},"setIdx":2,"setId":1,"iconIdx":22}],"height":1024,"metadata":{"name":"wled122"},"preferences":{"showGlyphs":true,"showCodes":true,"showQuickUse":true,"showQuickUse2":true,"showSVGs":true,"fontPref":{"prefix":"i-","metadata":{"fontFamily":"wled122","majorVersion":1,"minorVersion":7},"metrics":{"emSize":1024,"baseline":20,"whitespace":0},"embed":false,"autoHost":true,"noie8":true,"ie7":false,"showSelector":false,"showMetrics":false,"showMetadata":false,"showVersion":true},"imagePref":{"prefix":"icon-","png":true,"useClassSelector":true,"color":0,"bgColor":16777215,"name":"icomoon","classSelector":".icon"},"historySize":50,"quickUsageToken":{"wled122":"MDA1MGUxOTY0MyMxNzczNTY5NTMxI0ljcTJCSm9WQnFUUUFOdUZHQzlpaFZ0QkZaOGRFTXFRWFJoU1VjN2VqbmFn"},"showLiga":false,"gridSize":16}} \ No newline at end of file +{"IcoMoonType":"selection","icons":[{"icon":{"paths":["M902.213 804.010l-197.073-167.615c-20.373-18.336-42.161-26.752-59.761-25.941 46.52-54.492 74.622-125.188 74.622-202.455 0-172.313-139.687-312-312-312-172.311 0-312 139.687-312 312s139.687 312 312 312c77.266 0 147.962-28.1 202.455-74.624-0.812 17.6 7.605 39.388 25.941 59.761l167.615 197.073c28.698 31.887 75.58 34.574 104.178 5.976s25.913-75.48-5.974-104.178zM408 616c-114.874 0-208-93.125-208-208s93.125-208 208-208 208 93.125 208 208-93.125 208-208 208z"],"attrs":[],"isMulticolor":false,"isMulticolor2":false,"tags":["search","magnifier","magnifying-glass","inspect","find"],"defaultCode":59782,"grid":16},"attrs":[],"properties":{"ligatures":"search, magnifier","name":"search","id":134,"order":77,"prevSize":32,"code":57505},"setIdx":0,"setId":2,"iconIdx":134},{"icon":{"paths":["M910.398 765.581l-241.236-241.236c-14.934-14.934-39.371-14.934-54.306 0l-18.102 18.102-147.2-147.2 241.646-241.648h-256.001l-113.645 113.645-11.249-11.247h-54.306v54.306l11.247 11.247-164.848 164.849 127.999 127.999 164.848-164.848 147.2 147.2-18.102 18.102c-14.934 14.934-14.934 39.371 0 54.306l241.236 241.236c14.934 14.934 39.371 14.934 54.306 0l90.509-90.509c14.935-14.934 14.935-39.371 0.002-54.306z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"tags":["hammer","tool","fix","make","generate","work","build"],"grid":16},"attrs":[{}],"properties":{"order":1,"id":0,"name":"pixelforge","prevSize":32,"code":59648},"setIdx":1,"setId":1,"iconIdx":0},{"icon":{"paths":["M976.272 538.191c0 11.223-7.016 22.448-14.5 30.867l-157.14 185.202c-27.126 31.802-82.311 57.055-123.469 57.055h-508.837c-16.837 0-40.688-5.146-40.688-26.191 0-11.223 7.016-22.448 14.5-30.867l157.14-185.202c27.126-31.802 82.311-57.055 123.469-57.055h508.837c16.837 0 40.688 5.146 40.688 26.191zM815.856 377.307v74.828h-389.112c-58.461 0-130.952 33.208-168.835 78.104l-159.949 188.009c0-3.74-0.467-7.951-0.467-11.691v-448.977c0-57.523 47.233-104.761 104.761-104.761h149.66c57.523 0 104.761 47.233 104.761 104.761v14.968h254.418c57.523 0 104.761 47.233 104.761 104.761z"],"attrs":[{}],"width":1074,"isMulticolor":false,"isMulticolor2":false,"tags":["folder-open"],"grid":14},"attrs":[{}],"properties":{"order":1,"id":1,"prevSize":28,"name":"editor","code":59649},"setIdx":1,"setId":1,"iconIdx":1},{"icon":{"paths":["M511.573 85.333c235.947 0 427.094 191.147 427.094 426.667s-191.147 426.667-427.094 426.667c-235.52 0-426.24-191.147-426.24-426.667s190.72-426.667 426.24-426.667zM512 853.333c188.587 0 341.333-152.746 341.333-341.333s-152.746-341.333-341.333-341.333-341.333 152.746-341.333 341.333 152.746 341.333 341.333 341.333zM661.333 469.333c-35.413 0-64-28.586-64-64s28.587-64 64-64c35.414 0 64 28.587 64 64s-28.586 64-64 64zM362.667 469.333c-35.414 0-64-28.586-64-64s28.586-64 64-64c35.413 0 64 28.587 64 64s-28.587 64-64 64zM512 746.667c-99.413 0-183.893-62.294-218.027-149.334h436.054c-34.134 87.040-118.614 149.334-218.027 149.334z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE23D"],"defaultCode":57917,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":11,"order":26,"prevSize":32,"code":57917,"name":"pattern"},"setIdx":1,"setId":1,"iconIdx":2},{"icon":{"paths":["M511.573 791.040l314.88-244.907 69.547 54.187-384 298.667-384-298.667 69.12-53.76zM512 682.667l-384-298.667 384-298.667 384 298.667-69.973 54.187z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE34B"],"defaultCode":58187,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":14,"order":35,"ligatures":"","prevSize":32,"code":58187,"name":"segments"},"setIdx":1,"setId":1,"iconIdx":3},{"icon":{"paths":["M288.427 206.507l-60.587 60.16-76.373-76.374 60.16-60.16zM170.667 448v85.333h-128v-85.333h128zM554.667 23.467v125.866h-85.334v-125.866h85.334zM872.533 190.293l-76.373 76.374-60.16-60.16 76.373-76.374zM735.573 774.827l59.734-59.734 76.8 76.374-60.16 60.16zM853.333 448h128v85.333h-128v-85.333zM512 234.667c141.227 0 256 114.773 256 256 0 141.226-114.773 256-256 256s-256-114.774-256-256c0-141.227 114.773-256 256-256zM469.333 957.867v-125.867h85.334v125.867h-85.334zM151.467 791.040l76.373-76.8 60.16 60.16-76.373 76.8z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE333"],"defaultCode":58163,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":40,"order":73,"ligatures":"","prevSize":32,"code":58163,"name":"sun"},"setIdx":1,"setId":1,"iconIdx":4},{"icon":{"paths":["M512 128c212.053 0 384 152.747 384 341.333 0 117.76-95.573 213.334-213.333 213.334h-75.52c-35.414 0-64 28.586-64 64 0 16.213 6.4 31.146 16.213 42.24 10.24 11.52 16.64 26.453 16.64 43.093 0 35.413-28.587 64-64 64-212.053 0-384-171.947-384-384s171.947-384 384-384zM277.333 512c35.414 0 64-28.587 64-64s-28.586-64-64-64c-35.413 0-64 28.587-64 64s28.587 64 64 64zM405.333 341.333c35.414 0 64-28.586 64-64 0-35.413-28.586-64-64-64-35.413 0-64 28.587-64 64 0 35.414 28.587 64 64 64zM618.667 341.333c35.413 0 64-28.586 64-64 0-35.413-28.587-64-64-64-35.414 0-64 28.587-64 64 0 35.414 28.586 64 64 64zM746.667 512c35.413 0 64-28.587 64-64s-28.587-64-64-64c-35.414 0-64 28.587-64 64s28.586 64 64 64z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE2B3"],"defaultCode":58035,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":75,"order":75,"prevSize":32,"code":58035,"name":"palette"},"setIdx":1,"setId":1,"iconIdx":5},{"icon":{"paths":["M512 192c213.333 0 395.52 132.693 469.333 320-73.813 187.307-256 320-469.333 320s-395.52-132.693-469.333-320c73.813-187.307 256-320 469.333-320zM512 725.333c117.76 0 213.333-95.573 213.333-213.333s-95.573-213.333-213.333-213.333-213.333 95.573-213.333 213.333 95.573 213.333 213.333 213.333zM512 384c70.827 0 128 57.173 128 128s-57.173 128-128 128-128-57.173-128-128 57.173-128 128-128z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE0E8"],"defaultCode":57576,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":172,"order":74,"prevSize":32,"code":57576,"name":"eye"},"setIdx":1,"setId":1,"iconIdx":6},{"icon":{"paths":["M640 42.667v85.333h-256v-85.333h256zM469.333 597.333v-256h85.334v256h-85.334zM811.947 315.307c52.48 65.706 84.053 148.906 84.053 239.36 0 212.053-171.52 384-384 384s-384-171.947-384-384c0-212.054 171.947-384 384-384 90.453 0 173.653 31.573 239.787 84.48l60.586-60.587c21.76 17.92 41.814 38.4 60.16 60.16zM512 853.333c165.12 0 298.667-133.546 298.667-298.666s-133.547-298.667-298.667-298.667-298.667 133.547-298.667 298.667 133.547 298.666 298.667 298.666z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE325"],"defaultCode":58149,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":370,"order":21,"ligatures":"","prevSize":32,"code":58149,"name":"speed"},"setIdx":1,"setId":1,"iconIdx":7},{"icon":{"paths":["M707.84 366.507l60.16 60.16-256 256-256-256 60.16-60.16 195.84 195.413z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE395"],"defaultCode":58261,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":549,"order":69,"ligatures":"","prevSize":32,"code":58261,"name":"expand"},"setIdx":1,"setId":1,"iconIdx":8},{"icon":{"paths":["M554.667 128v426.667h-85.334v-426.667h85.334zM760.747 220.587c82.773 70.4 135.253 174.506 135.253 291.413 0 212.053-171.947 384-384 384s-384-171.947-384-384c0-116.907 52.48-221.013 135.253-291.413l60.16 60.16c-66.986 54.613-110.080 137.813-110.080 231.253 0 165.12 133.547 298.667 298.667 298.667s298.667-133.547 298.667-298.667c0-93.44-43.094-176.64-110.507-230.827z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE08F"],"defaultCode":57487,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":557,"order":19,"ligatures":"","prevSize":32,"code":57487,"name":"power"},"setIdx":1,"setId":1,"iconIdx":9},{"icon":{"paths":["M816.64 551.936l85.504 67.584c8.192 6.144 10.24 16.896 5.12 26.112l-81.92 141.824c-5.12 9.216-15.872 12.8-25.088 9.216l-101.888-40.96c-20.992 15.872-44.032 29.696-69.12 39.936l-15.36 108.544c-1.024 10.24-9.728 17.408-19.968 17.408h-163.84c-10.24 0-18.432-7.168-20.48-17.408l-15.36-108.544c-25.088-10.24-47.616-23.552-69.12-39.936l-101.888 40.96c-9.216 3.072-19.968 0-25.088-9.216l-81.92-141.824c-4.608-8.704-2.56-19.968 5.12-26.112l86.528-67.584c-2.048-12.8-3.072-26.624-3.072-39.936s1.536-27.136 3.584-39.936l-86.528-67.584c-8.192-6.144-10.24-16.896-5.12-26.112l81.92-141.824c5.12-9.216 15.872-12.8 25.088-9.216l101.888 40.96c20.992-15.872 44.032-29.696 69.12-39.936l15.36-108.544c1.536-10.24 9.728-17.408 19.968-17.408h163.84c10.24 0 18.944 7.168 20.48 17.408l15.36 108.544c25.088 10.24 47.616 23.552 69.12 39.936l101.888-40.96c9.216-3.072 19.968 0 25.088 9.216l81.92 141.824c4.608 8.704 2.56 19.968-5.12 26.112l-86.528 67.584c2.048 12.8 3.072 26.112 3.072 39.936s-1.024 27.136-2.56 39.936zM512 665.6c84.48 0 153.6-69.12 153.6-153.6s-69.12-153.6-153.6-153.6-153.6 69.12-153.6 153.6 69.12 153.6 153.6 153.6z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE0A2"],"defaultCode":57506,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":562,"order":29,"ligatures":"","prevSize":32,"code":57506,"name":"settings"},"setIdx":1,"setId":1,"iconIdx":10},{"icon":{"paths":["M556.8 417.707l125.867 94.293-256 192v-384zM556.8 417.707l125.867 94.293-256 192v-384zM556.8 417.707l-130.133-97.707v384l256-192zM469.333 173.653c-62.293 7.68-119.040 32.427-166.4 69.12l-60.586-61.013c63.146-51.627 141.226-85.76 226.986-94.293v86.186zM242.773 302.933c-36.693 47.36-61.44 104.107-69.12 166.4h-86.186c8.533-85.76 42.666-163.84 94.293-226.986zM173.653 554.667c7.68 62.293 32.427 119.040 69.12 165.973l-61.013 61.013c-51.627-63.146-85.76-141.226-94.293-226.986h86.186zM242.347 842.24l60.586-61.013c47.36 36.693 104.107 61.44 166.4 69.12v86.186c-85.333-8.533-163.84-42.666-226.986-94.293zM938.667 512c0 220.16-167.254 401.92-381.867 424.533v-86.186c167.253-22.187 296.533-165.547 296.533-338.347s-129.28-316.16-296.533-338.347v-86.186c214.613 22.613 381.867 204.373 381.867 424.533z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE139"],"defaultCode":57657,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":595,"order":46,"ligatures":"","prevSize":32,"code":57657,"name":"playlist"},"setIdx":1,"setId":1,"iconIdx":11},{"icon":{"paths":["M386.4 93.333c231.104 0 418.667 187.563 418.667 418.667s-187.563 418.667-418.667 418.667c-43.96 0-85.827-6.699-125.6-19.259 169.979-53.171 293.067-211.845 293.067-399.408s-123.088-346.237-293.067-399.408c39.773-12.56 81.64-19.259 125.6-19.259z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE2A2"],"defaultCode":58018,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":607,"order":34,"ligatures":"","prevSize":32,"code":58018,"name":"night"},"setIdx":1,"setId":1,"iconIdx":12},{"icon":{"paths":["M512 85.333c235.947 0 426.667 190.72 426.667 426.667s-190.72 426.667-426.667 426.667-426.667-190.72-426.667-426.667 190.72-426.667 426.667-426.667zM725.333 665.173l-153.173-153.173 153.173-153.173-60.16-60.16-153.173 153.173-153.173-153.173-60.16 60.16 153.173 153.173-153.173 153.173 60.16 60.16 153.173-153.173 153.173 153.173z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE38F"],"defaultCode":58255,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":662,"order":50,"ligatures":"","prevSize":32,"code":58255,"name":"cancel"},"setIdx":1,"setId":1,"iconIdx":13},{"icon":{"paths":["M512 170.667c188.587 0 341.333 152.746 341.333 341.333 0 66.987-19.626 129.28-52.906 181.76l-62.294-62.293c19.2-35.414 29.867-76.374 29.867-119.467 0-141.227-114.773-256-256-256v128l-170.667-170.667 170.667-170.666v128zM512 768v-128l170.667 170.667-170.667 170.666v-128c-188.587 0-341.333-152.746-341.333-341.333 0-66.987 19.626-129.28 52.906-181.76l62.294 62.293c-19.2 35.414-29.867 76.374-29.867 119.467 0 141.227 114.773 256 256 256z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE116"],"defaultCode":57622,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":709,"order":17,"ligatures":"","prevSize":32,"code":57622,"name":"sync"},"setIdx":1,"setId":1,"iconIdx":14},{"icon":{"paths":["M384 689.92l451.84-451.413 60.16 60.16-512 512-238.507-238.507 60.587-60.16z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE390"],"defaultCode":58256,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":733,"order":56,"ligatures":"","prevSize":32,"code":58256,"name":"confirm"},"setIdx":1,"setId":1,"iconIdx":15},{"icon":{"paths":["M853.333 370.773l141.227 141.227-141.227 141.227v200.106h-200.106l-141.227 141.227-141.227-141.227h-200.106v-200.106l-141.227-141.227 141.227-141.227v-200.106h200.106l141.227-141.227 141.227 141.227h200.106v200.106zM512 768c141.227 0 256-114.773 256-256s-114.773-256-256-256-256 114.773-256 256 114.773 256 256 256zM512 341.333c94.293 0 170.667 76.374 170.667 170.667s-76.374 170.667-170.667 170.667-170.667-76.374-170.667-170.667 76.374-170.667 170.667-170.667z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE2A6"],"defaultCode":58022,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":785,"order":15,"ligatures":"","prevSize":32,"code":58022,"name":"brightness"},"setIdx":1,"setId":1,"iconIdx":16},{"icon":{"paths":["M85.333 725.333v-42.666h128v170.666h-128v-42.666h85.334v-21.334h-42.667v-42.666h42.667v-21.334h-85.334zM128 341.333v-128h-42.667v-42.666h85.334v170.666h-42.667zM85.333 469.333v-42.666h128v38.4l-76.8 89.6h76.8v42.666h-128v-38.4l76.8-89.6h-76.8zM298.667 213.333h597.333v85.334h-597.333v-85.334zM298.667 810.667v-85.334h597.333v85.334h-597.333zM298.667 554.667v-85.334h597.333v85.334h-597.333z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE22D"],"defaultCode":57901,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":797,"order":58,"ligatures":"","prevSize":32,"code":57901,"name":"nodes"},"setIdx":1,"setId":1,"iconIdx":17},{"icon":{"paths":["M810.667 554.667h-256v256h-85.334v-256h-256v-85.334h256v-256h85.334v256h256v85.334z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE18A"],"defaultCode":57738,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":803,"order":59,"ligatures":"","prevSize":32,"code":57738,"name":"add"},"setIdx":1,"setId":1,"iconIdx":18},{"icon":{"paths":["M128 736l471.893-471.893 160 160-471.893 471.893h-160v-160zM883.627 300.373l-78.080 78.080-160-160 78.080-78.080c16.64-16.64 43.52-16.64 60.16 0l99.84 99.84c16.64 16.64 16.64 43.52 0 60.16z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE2C6"],"defaultCode":58054,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":834,"order":72,"ligatures":"","prevSize":32,"code":58054,"name":"edit"},"setIdx":1,"setId":1,"iconIdx":19},{"icon":{"paths":["M576 28.587c166.827 133.546 277.333 338.773 277.333 568.746 0 188.587-152.746 341.334-341.333 341.334s-341.333-152.747-341.333-341.334c0-144.213 51.626-276.906 137.813-379.306l-1.28 15.36c0 87.893 66.56 159.146 154.88 159.146 87.893 0 145.493-71.253 145.493-159.146 0-91.734-31.573-204.8-31.573-204.8zM499.627 810.667c113.066 0 204.8-91.734 204.8-204.8 0-59.307-8.534-117.334-25.174-172.374-43.52 58.454-121.6 94.72-197.12 110.080-75.093 15.36-119.893 64-119.893 133.12 0 74.24 61.44 133.974 137.387 133.974z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE409"],"defaultCode":58377,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":871,"order":10,"ligatures":"","prevSize":32,"code":58377,"name":"intensity"},"setIdx":1,"setId":1,"iconIdx":20},{"icon":{"paths":["M938.667 394.24l-232.534 201.813 69.547 299.947-263.68-159.147-263.68 159.147 69.973-299.947-232.96-201.813 306.774-26.027 119.893-282.88 119.893 282.454zM512 657.067l160.853 97.28-42.666-182.614 141.653-122.88-186.88-16.213-72.96-172.373-72.533 171.946-186.88 16.214 141.653 122.88-42.667 182.613z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE410"],"defaultCode":58384,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":927,"order":9,"ligatures":"","prevSize":32,"code":58384,"name":"star"},"setIdx":1,"setId":1,"iconIdx":21},{"icon":{"paths":["M512 85.333c235.52 0 426.667 191.147 426.667 426.667s-191.147 426.667-426.667 426.667-426.667-191.147-426.667-426.667 191.147-426.667 426.667-426.667zM554.667 725.333v-256h-85.334v256h85.334zM554.667 384v-85.333h-85.334v85.333h85.334z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE066"],"defaultCode":57446,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":952,"order":62,"ligatures":"","prevSize":32,"code":57446,"name":"info"},"setIdx":1,"setId":1,"iconIdx":22},{"icon":{"paths":["M256 810.667v-512h512v512c0 46.933-38.4 85.333-85.333 85.333h-341.334c-46.933 0-85.333-38.4-85.333-85.333zM810.667 170.667v85.333h-597.334v-85.333h149.334l42.666-42.667h213.334l42.666 42.667h149.334z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE037"],"defaultCode":57399,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":969,"order":55,"ligatures":"","prevSize":32,"code":57399,"name":"del"},"setIdx":1,"setId":1,"iconIdx":23},{"icon":{"paths":["M704 128c131.413 0 234.667 103.253 234.667 234.667 0 161.28-145.067 292.693-364.8 491.946l-61.867 56.32-61.867-55.893c-219.733-199.68-364.8-331.093-364.8-492.373 0-131.414 103.254-234.667 234.667-234.667 74.24 0 145.493 34.56 192 89.173 46.507-54.613 117.76-89.173 192-89.173zM516.267 791.467c203.093-183.894 337.066-305.494 337.066-428.8 0-85.334-64-149.334-149.333-149.334-65.707 0-129.707 42.24-151.893 100.694h-79.787c-22.613-58.454-86.613-100.694-152.32-100.694-85.333 0-149.333 64-149.333 149.334 0 123.306 133.973 244.906 337.066 428.8l4.267 4.266z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE04C"],"defaultCode":57420,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":1034,"order":61,"ligatures":"","prevSize":32,"code":57420,"name":"presets"},"setIdx":1,"setId":1,"iconIdx":24}],"height":1024,"metadata":{"name":"wled122"},"preferences":{"showGlyphs":true,"showCodes":true,"showQuickUse":true,"showQuickUse2":true,"showSVGs":true,"fontPref":{"prefix":"i-","metadata":{"fontFamily":"wled122","majorVersion":1,"minorVersion":7},"metrics":{"emSize":1024,"baseline":20,"whitespace":0},"embed":false,"autoHost":true,"noie8":true,"ie7":false,"showSelector":false,"showMetrics":false,"showMetadata":false,"showVersion":true},"imagePref":{"prefix":"icon-","png":true,"useClassSelector":true,"color":0,"bgColor":16777215,"name":"icomoon","classSelector":".icon"},"historySize":50,"quickUsageToken":{"UntitledProject1":"NmE0ZDcwNGYzYyMxNzc2MDA4Njk4I2J4UGhvcTgwUS9WOElreWNhMkRVS1BlaGlSRGFJVTVqMFR1Ykhqa1lqc1B4"},"showLiga":false,"gridSize":16,"showGrid":true}} \ No newline at end of file diff --git a/wled00/data/icons-ui/style.css b/wled00/data/icons-ui/style.css index 829559fffa..28ad642e32 100644 --- a/wled00/data/icons-ui/style.css +++ b/wled00/data/icons-ui/style.css @@ -1,9 +1,9 @@ @font-face { font-family: 'wled122'; src: - url('fonts/wled122.ttf?yzxblb') format('truetype'), - url('fonts/wled122.woff?yzxblb') format('woff'), - url('fonts/wled122.svg?yzxblb#wled122') format('svg'); + url('fonts/wled122.ttf?2tjc6') format('truetype'), + url('fonts/wled122.woff?2tjc6') format('woff'), + url('fonts/wled122.svg?2tjc6#wled122') format('svg'); font-weight: normal; font-style: normal; font-display: block; @@ -24,6 +24,9 @@ -moz-osx-font-smoothing: grayscale; } +.i-search:before { + content: "\e0a1"; +} .i-pixelforge:before { content: "\e900"; } diff --git a/wled00/data/index.css b/wled00/data/index.css index f3a9f8466a..f1cab8f615 100644 --- a/wled00/data/index.css +++ b/wled00/data/index.css @@ -1,6 +1,6 @@ @font-face { font-family: "WIcons"; - src: url(data:font/woff2;charset=utf-8;base64,d09GMgABAAAAAAsAAA0AAAAAFlgAAAqqAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP0ZGVE0cGhgGYACEGhEICp08lnkLPgABNgIkA3gEIAWDGweCNhvxEVGUcFI3wBeFsYOmlCFXadeSCl4PGhMTwyMh0q9d2MXuDaeszCMkmT3Abd0Eu2ijAIMUa1IDbaQmRj/wndtnJB+d8BHN/+ZKv+zJJpUCCAsMA5IcArBbtlteAg6ToYi3nPp6KxH97fd9OQgssMYTSymghAPMMODmLNpvv/P8BPzeodosVKppyCRNZE0QEqlTCp0SqP9T4O4gAMzzFuTJg2RPa6/23s/f4IYKREKfr6tTc/cLu7dh2JTwmhJdUiSLQqZVQFvmy6mScazQAwlZ7apjDAOl7l8dYEyN5azo7xRYCTCz7gCAzIa7hoI38uBn9/NfQMIrA5RCyCOfOtya0oEneAKP2+M8AEzujgX5QIQYkXEhC5nk4BVC6f6L4cmN4YazURxLPmVQjD4XkFWhNcfmv38+EMNisJkOyOKfgx6n/2z9efLjZY9fPol6EvJEdaY7I5y1zu3Ok64kl58r7bcprplPfZ+GvELuPwEAiGmvZJPj8ErdT9kXF+1jV7AvsG3seNY31uuFw5m/LLgKwNzGLGd8mO+cfw6A8S5jCsM/9wfEH8iWrJEYBLUxMHfsLJpcHQqzOuDEFhQjM1otoVvVg94O/zMIoCJtI1ACwThSfr8yQL1KvQ5rAApCJOJJKBSl4cdB3IwhcY7A5i3/hNDuIJq7NmfVLJNq2Z1hACMTkEpSDwPzGMtL6Qj7EFl8BemVw4zAppSRHW5ZhSxVZIZwKIDXduoxP57T0cgYeukZbC1afoHHq6/OwUSERJEC0lcLGXjp0QKyd7tOLYzdaXLTFHYixavtddgQ0YyI6xbZbLleW+DKSDGxqrvjTWIRtNNgOF6yGYc0ZhihH0R1vR9WuWn2Q4pkWdcmW0QsbEIYzglYJKxhzbvPBSWhn9uiMsuraZ3jiQ75dBgpD4mW9tgSdSHFzLzEcnLiNDvb59n9lVxzrObWObWDviOG3Dwt5RZCKdLLyl04L0+xvKG6aEG0nJFTM6AcuXROdpzmFJCcH9+uWfmohYxDH0Nxk+nRN4ZT3uJW3O32b9GChl57lSFlYeur0F6s+ve/cC8GeUHLy5CeTZoB7XGeFaxDWspDQ9CBaXdnUZU9hGerGTqIgUtgQxhFauojOOdYXo78csyahwycYlRk/FVxQdrYrQc7r1tJQJv1+Xi5FbW+xPCwj5pLicU1YATAPRM9hVc9RfdxWc2300x9lIgM3K/9xgtYHI8miESYICECeMSQt3EtAdq7jhUlLE2CiYgNqUeZNrzc9nLTTg+EeckP9Kz28vnwTeoolOtCGyF5WOonuVZNPkHX/RKff2/l48rnCUbIfJZad59cYhSwkWPEJUQkRvZrYUMVbAKCS6jB/bp7M2ItABfEMpgBinhBFLgze5jkAlW62xjORdV67XqlRsPsObLU7cI+/4ss97HdGJ2iXMrTFMuRTzAe2SISYd9NlE6rZmS4ahqS+8GKTA7ZuWs9YGQfYGQHdUqbXcy+iQC2aiEDhkdLTkhvpoYOmp6tTc6yvgVbEIGdkoPu2sV275V27N23h7awKFxyUm5n1CGxXfscu7nrlINyF7v00vEyotuwG5If4LpYtazK+s3xmi4bpC2UoPNVnRa9JubCZj3+jg4Zl+iGnds38V2bNqxnXOKcUkYv8Vw1vppL4+lMDDMok9jqbFmxHE1LeYp/Sc6O03odj1droeRpqckiE7Qa4jB+nO5OlVyIymtCtJdACJKcTKe3Kct4DL+2knGWW/gpzKXr191XULH0Ay1NmD9ndUMvJaoqrCq5dStqFaosxPyr8/N902gfWD5BcFtmaqreo8wxq1T1+g6+d8iQDLnRJBeYZP4jf/MEBpHR0Lj1zmvSecXw/+vqjLhyTs+enLSoujoiRy3LDbIhvmtxCTAzTZPZBNzr683+Pi7U/TOZjE+Z8yHfzlQzMbsdS4t1ulIwTTJN6/hj5nBM5GevHDFhfTVob+tnthVHUVyu6o1q8GeQCn5TYowqQ4a0asLK33fsSX2zLCVo473WZ4XPWu/gTUr4n1nSfH38mHmqzKpYCucxNo9yXJO1toU1NYX8GuAm7EXRRVH9ja9f0zCPBxUQoNvXeb64MoLftWmu23d39+9eBU1d+UObPKOkpETCw4F7hvbO3brNG0u0Qnrt6B9fveVI0AIMu+aSkOtc3VrSJG5IwMsAv2Rwvfs6ObS5xyXIGfFGlW5cxjv/b4+s7/gTclsCLce7ZvXo6i3rJxi2P9ln4irW+XW89OtSmD3FBmYRo9jaDUvEEip98Bf1mytr7BaFwmJXXVf/AsfRQx8c8MBdywDCjkgAM7s2GDeXXEdyeRSPy4viEmSqzesYgTclp1nKvv50S/kNN+Me01EF0wbWprFZyoBXWACDKu3Cljz17p1WbIZ7xFwjnWai0bGQqsZQK2xf3jggsrSXIVaxQ5EaS2GoE0/jlHG6deccNaU4PqGWZWrG4+588wUzl9saGzWaiLzKjH1B/XEQ4LgwcYIvPn16iYkW1K9gpBLXayyhAJWUWWu2o4jXaVtbtfzXgfuQTk3DaPbaBw817l7OvamJX0Yz0gPOtn4jx9N79MYQbCTF84i+sxz6kXTj3MYcbvkx56XzGsMoWng/EOvWrcWLo2/Jki/by8srSCjHsse7du1fBqtFNQfTTAMOYnfw+6srmZgvttlWFUunU+SoXWtJpU4qtduaqVndnxftCHhw2c5Qs43pa9cbRfu0y1Nt5oTN6hPvfS+w6LgjvexcaoGJZO76IeYh02unz5FWVjqiKer9q78ieyU0Da60eLSoAM296/BJHbMKCIXs4Xs17vLgTs35ikkIrh9BLc4dTXAxNvU5UvV1Vb7bhkO4BhD/9lGHO+/fn4NjlwtHhQO8BSSK5a9HRtGUqfZwnbmeTb2rTbpb764lHTY8Ydt87VtQbHW9UlkZn5WaPRqobxB3qLN+/cb18J+f+dNROn5AISbO1lVAbseul2ewdd4vjwdVkzC2L02fKWdJE3fnxAH7JhVtSF4/EDxhQNoukP0c++bTOk5j6JfTPn6EbndfYOD6FcsJIgKUob1Inz6u5zRZLPsWD0IB4t4DWzCg1XLY/wIGg30NHTTauPsJKVtSOtJ9O2/rYgfF03zzHqybNYqD/yx4tforP6Ld9vAr2ybl/3yIRTcdrwzuetFFSSMAH0LMxI2+fkDdCcDYJyA3ipitETmBOLi8EZmJSOpOPFq/DTxMGhrE3JLs83kymayp5Uh8Ms2xDiHtOJqBLNjEdz8eyLwgrYDkpX6syTp5sNVEYdFEZesHeyLOS68ey57lZy682pmLOqOJ4wcS2GwSmTfZWPLgMWbCdumm7N32YP5QDH110k4bAfiCL0Df065NIHyl/q626c2Y16wHeIviHYE4G+iT5oGtK/bUXlddcGyeJwQBPKxxgIKM7PhKE0/2uuQ+juqSmmzG3PDQFXfqjwMpWpmyPLpjTQbA8zda3OddU9za9W/xDBTYht7SfiikklBMEosFGw5ceGBX1J+TRABBhBBGBFHEEIeCD40EkkghjYx77NI+y02QY4JeWJYom4tVXCrlMg1XCDMwWSeBQMpFORkyRSehUM1EmQphXMqVogyVtJNIKOEiERcruUyTmZOVJOkkzsrJEGRl5WR5AgA=); + src: url(data:font/woff2;charset=utf-8;base64,d09GMgABAAAAAAtwAA0AAAAAFzwAAAsYAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP0ZGVE0cGhgGYACEHhEICp8QmBQLQAABNgIkA3wEIAWDGweCQBueEhEVqmUn+JmQuYW8aYRI8pyRytrpySK/jJBk9v+nbf19MKDMoI0Jwi77VwXMwsTIUSbEKvy5Veevw/8NG9XGRiobEQNstPsjEh74P+bfFxu1zQc2m+Aa+1eIVHCCRR1PoA82/wK/+v91bBjrxhAh4ktd/2kus/ntTjFbOEASKufInVMvm8JMpgyzruVsD5AssAcF1PIhpKsOUGhW6IGMOOvPDZlh7zJjzjrI26wbrOpZbwBAVfDQuPpH/fDP0TubwXIgpEa+JEjFsKsLM0KLQAQE14LrALAQORH0B0lBAopZoRo0lge+QsIkt7t+gH1ODaQKTBViUo7GA2pIRP8bMf8CyVQwgRDVMu155fOe5wufb3n+5YugFxkvrC9jXua83PHy4MuLo6+NJo1WjC4aXf5K8sr3K+Q2AIDnqTcbt7CFHniq7lfKm8oR5WblLABlM+L3CD7iA8VLxT0AxURRLf9RPiq/DiA/kPvLpQUwrWTSrYAbx6aq0GK5CHxuIQmcHz2lvxChFCIiUFFHKRJF8CoU/zMIYJApEPQffEglRnqtOgL79YqvGCYRhokEXIhkfaAnREw0kRC6CCNxkuxFQzw93OKS1xQKDBMlauQ1UrlcGhaKYZjEUyJRSqViQtRxRbSQ1FPtK/eKlmJsMY5hKgXG5TMXt6G2rDgyAMcjgoMR7poV+WDcbVxqvTsDvAORS+QQM2wZw3diBoGL38dToxuyzsk4nYkHkCVu770sO2Fz5JxyEU0u4z1XZVFkceJWXocPaTOA+92Io/YOLXvTRjscz0HAUTw/CLcyTucgZGFdrosTzUJc0IhwmBFxIUHvjr52qVjv4YUrN7ri+rW113ngEvY7yq4QtoprrqSDyOKsnsOs2IGfciFL4gFn+VXcWnqZj9/HvBpXLrdaRu++oO3foumwpo7cjd2ALMq997KOEraU4Tsx64GTD9xOPzSHXr48kFq2LIhZdmycVzhuXaHidowEmWhkxcpgdsUaZFkbvGorAleulNEr7ZFQBGFbruK2D8tMtL58WSC1fDVwqwLbbK4CKQbOkQsT5J+h+bzToxWcZVyF5zrfbEuz34vbiCwRQ3czj+DW5GO3deuAUwzeyTjcaHMA929BJ7K43rSVXXFcNw8cr6tbuxVZYoecmSM4MCPAAk0h62CVTLUrrnbVpsJvmDE8haaEtPppMQK24RCfXnfKVQLUAy/Ht7py69c7YsiTzrGEyTjsSIGLTv2Ugi+m0KUCTszXkmLkqMCLzykJcKEUeQkJgReLCMA5BNkNdBZQqM90YRZnfcA7YMjsRKx5A0gVV+Zqy/DDG60nkeUUYTsB3HEpdJomq5Px4nk8hLqL46/V3PI1iOqb5TsvEXNoCvA+gyOGRQRLE722LHtdTSzEuazpx3jdQKGtlOucGKNpB9r47WaOh1XHgVstq+BNHM9IQb3tlJAnne14goWMhJH+i7BmugtHUyiGXWmT0cu5OSANzUJCYD5TUGfV5qgybt8d2ptjQRNvkoyxHvBgBjy0U3Vsk5EmGwnAV20Zw46YwcwQ38CaOeKMHWq9mutb0IwI6BjVUp2ddFcP295j7qa66k0xbB3FkDRVy9Kd5touxjilliqq6Dl3au+49vURZaWX8eZYoN7q2qvjAjkH0R2y0Mz6TsrXxBkxwBed54FzzKFMHW2NTGejqZ5mWIahKJqaw6ywBkYxrJQKwkHWp1k6ahkno5fbgtmVlqA53LJAahePS607kGVlMNvvIrxs24EhPeV1lGUFElruEYTtDoBmRUWcO5BS19UMu9dsoRl6xRrnmhVw6lTC5s0/hEUnZq/a2TK1QF+YW1vucrEq1BfkaufOVdmJ0WE/JB0J6V1xUVF2k+n2FRWFXz9ge+bOjc2j6LwEOp0gidFOMOSxVcX2PPi6sclkcH9dXzWmSjVliqrMqK9neChRNQJrYn0xTR8w6D7N0KD56GfoFm40fbWQG1u26hMdqShSGHp7DWUmSb5xxSgUoyJ1n6zKNcYt/MoekjWjWD+tZ6nqYfV6Vl8wTV8c3UwUpE/k53d++613ZKQ5M//jmRTFKj9/5scGAInwr840R072vnlz53n2nIHKnvadXdtKzhD9ptX+JppBtmop95BUD6EKjbGptfPGv2rvjvpGHen9psduz22euz183owM+OfOaudtfcxiC5nCUCGOB0hodycVSuIn5TY05KovAu4+sjS4NGgW9fXXYYYJJwQkgs2Tll9cE6ib0LZa/LtO97t4ddsEHXZMLZVSXp6idgfuZSUPHjY1Da4vSQxv/PjpV9+qUvQ5BsO90RRuz6NkN0dr18R2G6C7f7fZfYqztKOYOTcE6ajlKbX2+u+T439nX3AH1FQVOyF+8gSP9Cq18eB1toItjLiuzUbxrJtjZUZFqbysZ6JhnEFd48l+0dxU09Brys839Zpqav6CjRAjgxyyyC9jhMRXGQCjvDhQmxpuD6dhgrQabZCmjysVs4fui/zjK0yVm7PSVOVwDxjCYJoEYg3r6MxMlYjyTFAMBSXrB33EwwergBt51BdGhYfRYeGGZ0SRAUoa2AWp2bKKqcbQ/EOGstEKsLxkK1SVqomTVF1RBcsSUKLSGmfHPHjwi4GpMre2FhczfAFj+CVwjoGUzo358yjz8uWVzx9XhNqhqJTRewZDjj6l2lYFHyd5T0lnZ4n614H7FFkUZgjrbXzytFgnFn/a1qarDKPCpR+aZ34oDS8K72ArdrzMNX0ZczW/HBhOrd6TyQ1/zHz2mj3OZJgXJ/SNXa6NG9mxadMvB6uqBODKrr7W2XlcDVFJRgPdjwYGhE51s4pqFIYp02xeCyJI0uChfBZZNaQuu9fc9W+uaJY22C59cruX1jNmkuyeptdP7/YRZsaCtemeaTcDyn17ZeW1qByao9275jJzl+4YHPaKyooP+lkdj1rKxVlUdJ5FFukzcvTe5vNQ8FrLIT2wg+Su9wXBHdX+1ZDSpyF/UXLmx94E9Lru5wr5wlWPTdg1F4G+i3Nn7e7ff38Nzt1+MY3bK1eBcUn9Iia2dJE/GUAyTa3gYUkJW03vYqnvva9uXWv7zbugvrmgoAYPq0j5MWA/3poasufX3zQxBH3lnzdTbbNzDclpZDVkvh758opB67FhwgnhTwydEWYPVbWpTXN4/uwRn9KB0v5hn3mzo49A+rLgx5/2qPb4LVny44+wvz+Smtq/bWtfH6Mi/OKgfvqxX9VvMo2oRiAn4J4LDeuGS0pUyv+kIz4Swy7FJWNuxXfjpooPdW3a/RvtQpHwjPaJdgVn+vw/rrjo/8rvSa6/ewdbfbJ/8yQw2NwGgrtZOCqMnqQnanm2tqWDAGsBIGWJKym6KTP0RAMwI+FgRhNHbTSJtNS/CdZpJCqQYkMOJW0qh9JiktoPV/fkRGhP1FClRpLdCCiqvQ0BwmoRD74QCVgUCVkeYax+uAhhphDHbFklLszd3/EoS8g7ASMBCyEhiyeMlQ4XCdImxMY+6Awu2TqY4S3duiRIlAhmbHd3JSReou4oNJuWqm03GfxIeUvA2xpaC6a/l8mmeN0E71BJECsNXL7sYUJsWhCy2KuYOgtzTr4TIG/XlhdY4x1mUSnTaYIqtrCiWHDPZKW9+aKyzgl/7/MoAAQUd6oEKIIgCEEYMCRCYuSBPJFkA/DIpMgLeSMf5Iv8kD8KQIFIhoJQMApBoSgMhSM5UnjIae98IFEfwn4XmrpvPULP5216LPpmNzJ0VXR2N7PD5G3zhL4UvaObbOiS7kvP5y2Ejm6OiiZiCjYBAAAA); } :root { diff --git a/wled00/data/index.js b/wled00/data/index.js index 3b79767797..ee5126973c 100644 --- a/wled00/data/index.js +++ b/wled00/data/index.js @@ -25,6 +25,7 @@ var pN = "", pI = 0, pNum = 0; var pmt = 1, pmtLS = 0; var lastinfo = {}; var isM = false, mw = 0, mh=0; +var bsOpts = null; // blending style options snapshot, used for dynamic filtering based on matrix mode (iOS compatibility) var ws, wsRpt=0; var cfg = { theme:{base:"dark", bg:{url:"", rnd: false, rndGrayscale: false, rndBlur: false}, alpha:{bg:0.6,tab:0.8}, color:{bg:""}}, @@ -310,10 +311,10 @@ function onLoad() sl.addEventListener('touchstart', toggleBubble); sl.addEventListener('touchend', toggleBubble); }); - // limiter for all number inputs: limit inputs instantly + // limiter for all number inputs except segment inputs: limit inputs instantly note: segment inputs are special if matrix is enabled, they allow for trailing strips, need a lot of special cases to handle that d.addEventListener("input", function(e) { const t = e.target; - if (t.tagName === "INPUT" && t.type === "number") { + if (t.tagName === "INPUT" && t.type === "number" && !(t.id && t.id.startsWith("seg"))) { let val = parseFloat(t.value); const max = parseFloat(t.max); const min = parseFloat(t.min); @@ -660,12 +661,14 @@ function parseInfo(i) { mw = i.leds.matrix ? i.leds.matrix.w : 0; mh = i.leds.matrix ? i.leds.matrix.h : 0; isM = mw>0 && mh>0; + if (!bsOpts) bsOpts = Array.from(gId('bs').options).map(o => o.cloneNode(true)); // snapshot all options on first call + const bsSel = gId('bs'); + // note: style.display='none' for option elements is not supported on all browsers (notably iOS) + bsSel.replaceChildren(...bsOpts.filter(o => isM || o.dataset.type !== "2D").map(o => o.cloneNode(true))); // allow all in matrix mode, filter 2D blends otherwise if (!isM) { - gId("filter2D").classList.add('hide'); - gId('bs').querySelectorAll('option[data-type="2D"]').forEach((o,i)=>{o.style.display='none';}); + gId("filter2D").classList.add('hide'); // hide 2D effects in non-matrix mode } else { gId("filter2D").classList.remove('hide'); - gId('bs').querySelectorAll('option[data-type="2D"]').forEach((o,i)=>{o.style.display='';}); } gId("updBt").style.display = (i.opt & 1) ? '':'none'; // if (i.noaudio) { @@ -986,21 +989,39 @@ function populatePalettes() ); } gId('pallist').innerHTML=html; - // append custom palettes (when loading for the 1st time) + // append usermod palettes (fixed ID space: 255 down to 201) let li = lastinfo; - if (!isEmpty(li) && li.cpalcount) { - for (let j = 0; j
` ); } } + // append user custom palettes (fixed ID space: 200 down to FIXED_PALETTE_COUNT+1) + if (!isEmpty(li) && li.cpalcount) { + for (let j = 0; j < li.cpalcount; j++) { + const id = 200 - j; + const pd = palettesData[id]; + if (pd && pd.length === 16 && pd.every(e => e[1] === 128 && e[2] === 128 && e[3] === 128)) continue; // skip gray gap-placeholder entries + let div = d.createElement("div"); + gId('pallist').appendChild(div); + div.outerHTML = generateListItemHtml( + 'palette', + id, + '~ Custom '+j+' ~', + 'setPalette', + `
` + ); + } + } + updateSelectedPalette(selectedPal); // update selection after adding usermod and custom palettes } function redrawPalPrev() @@ -1182,7 +1203,7 @@ function updateLen(s) let mySD = gId("mkSYD"); if (isM) { // do we have 1D segment *after* the matrix? - if (start >= mw*mh) { + if (start >= mw*mh && s > 0) { if (sY) { sY.value = 0; sY.max = 0; sY.min = 0; } if (eY) { eY.value = 1; eY.max = 1; eY.min = 0; } sX.min = mw*mh; sX.max = ledCount-1; @@ -1457,7 +1478,9 @@ function readState(s,command=false) tr = s.transition; gId('tt').value = tr/10; - gId('bs').value = s.bs || 0; + const bsSel = gId('bs'); + bsSel.value = s.bs || 0; // assign blending style + if (!bsSel.value) bsSel.value = 0; // fall back to Fade if option does not exist if (tr===0) gId('bsp').classList.add('hide') else gId('bsp').classList.remove('hide') @@ -2812,7 +2835,7 @@ function loadPalettesData() { if (lsPalData) { try { var d = JSON.parse(lsPalData); - if (d && d.vid == lastinfo.vid) { + if (d && d.vid == lastinfo.vid && d.pcount == lastinfo.palcount) { palettesData = d.p; redrawPalPrev(); return resolve(); @@ -2824,7 +2847,8 @@ function loadPalettesData() { getPalettesData(0, () => { localStorage.setItem("wledPalx", JSON.stringify({ p: palettesData, - vid: lastinfo.vid + vid: lastinfo.vid, + pcount: lastinfo.palcount // total palette count, refresh cache if it changes })); redrawPalPrev(); setTimeout(resolve, 99); // delay optional @@ -3413,13 +3437,23 @@ function showVersionUpgradePrompt(info, oldVersion, newVersion) { function reportUpgradeEvent(info, oldVersion, alwaysReport) { showToast('Reporting upgrade...'); + const IR_TYPES = { + 0: null, // not configured — omit field entirely + 1: "24-key", // white 24-key remote + 2: "24-key-ct", // white 24-key with CW, WW, CT+, CT- keys + 3: "40-key", // blue 40-key remote + 4: "44-key", // white 44-key remote + 5: "21-key", // white 21-key remote + 6: "6-key", // black 6-key learning remote + 7: "9-key", // 9-key remote + 8: "json-remote", // ir.json configurable remote + }; - // Fetch fresh data from /json/info endpoint as requested - fetch(getURL('/json/info'), { - method: 'get' - }) - .then(res => res.json()) - .then(infoData => { + // Reuse the info argument and fetch only /json/cfg (serialize requests to avoid 503s on low-heap devices) + const infoData = info; + fetch(getURL('/json/cfg'), {method: 'get'}) + .then(res => res.ok ? res.json() : Promise.reject(new Error('Failed to fetch /json/cfg'))) + .then(cfgData => { // Map to UpgradeEventRequest structure per OpenAPI spec // Required fields: deviceId, version, previousVersion, releaseName, chip, ledCount, isMatrix, bootloaderSHA256 const upgradeData = { @@ -3434,13 +3468,58 @@ function reportUpgradeEvent(info, oldVersion, alwaysReport) { brand: infoData.brand, // Device brand (always present) product: infoData.product, // Product name (always present) flashSize: infoData.flash, // Flash size (always present) - repo: infoData.repo // GitHub repository (always present) - }; + repo: infoData.repo, // GitHub repository (always present) + fsUsed: infoData.fs?.u, // Filesystem used space in kB + fsTotal: infoData.fs?.t, // Filesystem total space in kB + + // LED hardware + busCount: cfgData.hw?.led?.ins?.length ?? 1, + busTypes: (cfgData.hw?.led?.ins ?? []).map(b => busTypeToString(b.type)), + matrixWidth: infoData.leds?.matrix?.w, + matrixHeight: infoData.leds?.matrix?.h, + ledFeatures: [ + ...(infoData.leds?.lc & 0x02 ? ["rgbw"] : []), + ...(infoData.leds?.lc & 0x04 ? ["cct"] : []), + ...((infoData.leds?.maxpwr ?? 0) > 0 ? ["abl"] : []), + ...(cfgData.hw?.led?.cr ? ["cct-from-rgb"] : []), + ...(cfgData.hw?.led?.cct ? ["white-balance"] : []), + ...((cfgData.light?.gc?.col ?? 1.0) > 1.0 || (cfgData.light?.gc?.bri ?? 1.0) > 1.0 ? ["gamma"] : []), + ...(cfgData.light?.aseg ? ["auto-segments"] : []), + ...((cfgData.light?.nl?.mode ?? 0) > 0 ? ["nightlight"] : []), + ], + + // peripherals (note: i2c/spi may reflect board defaults, not user-configured hardware) + peripherals: [ + ...((cfgData.hw?.relay?.pin ?? -1) >= 0 ? ["relay"] : []), + ...((cfgData.hw?.btn?.ins ?? []).filter(b => b.type !== 0).length > 0 ? ["buttons"] : []), + ...((cfgData.eth?.type ?? 0) > 0 ? ["ethernet"] : []), + ...((cfgData.if?.live?.dmx?.inputRxPin ?? 0) > 0 ? ["dmx-input"] : []), + ...((cfgData.hw?.ir?.type ?? 0) > 0 ? ["ir-remote"] : []), + ], + buttonCount: (cfgData.hw?.btn?.ins ?? []).filter(b => b.type !== 0).length, + + // integrations + integrations: [ + ...(cfgData.if?.hue?.en ? ["hue"] : []), + ...(cfgData.if?.mqtt?.en ? ["mqtt"] : []), + ...(cfgData.if?.va?.alexa ? ["alexa"] : []), + ...(cfgData.if?.sync?.send?.en ? ["wled-sync"] : []), + ...(cfgData.nw?.espnow ? ["esp-now"] : []), + ...(cfgData.if?.sync?.espnow ? ["esp-now-sync"] : []), + ], + + // usermods + usermods: Object.keys(cfgData.um ?? {}), + usermodIds: infoData.um ?? [], + }; + + // IR remote — only include if configured + const irType = IR_TYPES[cfgData.hw?.ir?.type ?? 0]; + if (irType) upgradeData.irRemoteType = irType; // Add optional fields if available if (infoData.psrSz !== undefined) upgradeData.psramSize = infoData.psrSz; // Total PSRAM size in MB; can be 0 - // Note: partitionSizes not currently available in /json/info endpoint // Make AJAX call to postUpgradeEvent API return fetch('https://usage.wled.me/api/usage/upgrade', { @@ -3471,6 +3550,17 @@ function reportUpgradeEvent(info, oldVersion, alwaysReport) { }); } +function busTypeToString(t) { + if (t === 0) return "none"; + if (t === 40) return "on-off"; + if (t >= 16 && t <= 39) return "digital"; // WS2812, SK6812, etc. + if (t >= 41 && t <= 47) return "pwm"; // analog RGB/CCT/single + if (t >= 48 && t <= 63) return "digital-spi"; // APA102, WS2801, etc. + if (t >= 64 && t <= 71) return "hub75"; // HUB75 matrix panels + if (t >= 80 && t <= 95) return "network"; // DDP, E1.31, ArtNet + return "unknown"; +} + function updateVersionInfo(version, neverAsk, alwaysReport) { const versionInfo = { version: version, diff --git a/wled00/data/pixelforge/pixelforge.htm b/wled00/data/pixelforge/pixelforge.htm index 165eb7a33d..e8fb27b674 100644 --- a/wled00/data/pixelforge/pixelforge.htm +++ b/wled00/data/pixelforge/pixelforge.htm @@ -424,7 +424,7 @@

Custom Fonts

const classics=['console_font_4x6.wbf','console_font_5x12.wbf','console_font_5x8.wbf','console_font_6x8.wbf','console_font_7x9.wbf']; // classic WLED fonts list let pT = []; // local tools list from JSON let wv = [0, 0]; // wled version [major, minor], updated in fsMem(), used to check tool compatibility -const remoteURL = 'https://dedehai.github.io/pftools.json'; // Change to your actual repo +const remoteURL = 'https://wled.github.io/wled-web-tools/pftools.json'; // tools list const toolsjson = 'pftools.json'; // note: the pftools.json must use major.minor for tool versions (e.g. 0.95 or 1.1), otherwise the update check won't work // also the code assumes that the tool url points to a gz file @@ -498,7 +498,7 @@

Custom Fonts

}).catch(console.error); } -/* which seg is showing image fx 53 */ +/* which seg is showing image fx (ID 53) */ function curImgSeg(){ const sel=getId('seg'); for(let i=0;i${esc(t.name)} v${esc(t.ver)} on:true, seg: cur!==null && cur!==tgt ? [{id:cur,fx:0,n:""},{id:tgt,fx:53,frz:false,sx:128,n:name}] - : {id:tgt,fx:53,frz:false,sx:128,n:name} + : {id:tgt,fx:53,frz:false,sx:128,ix:0,n:name} }; const r=await fetch(getURL('/json/state'),{method:'POST',body:JSON.stringify(j)}); const out=await r.json(); diff --git a/wled00/data/settings.htm b/wled00/data/settings.htm index ce3c246bad..ef20671c87 100644 --- a/wled00/data/settings.htm +++ b/wled00/data/settings.htm @@ -9,20 +9,21 @@ (function loadFiles() { const l = document.createElement('script'); l.src = 'common.js'; - // load style.css then initialize - l.onload = () => loadResources(['style.css'], () => { + // note: loading style.css here causes ugly white button flashes so @import url("style.css") below inlines it for this page (only costs ~30bytes of flash) + l.onload = () => { getLoc(); loadJS(getURL('/settings/s.js?p=0'), false); - }); + }; l.onerror = () => setTimeout(loadFiles, 100); document.head.appendChild(l); })();