diff --git a/.coderabbit.yaml b/.coderabbit.yaml
index 5be93fa226..312efe0e3e 100644
--- a/.coderabbit.yaml
+++ b/.coderabbit.yaml
@@ -6,6 +6,8 @@
# docs/cpp.instructions.md — C++ coding conventions
# docs/web.instructions.md — Web UI coding conventions
# docs/cicd.instructions.md — GitHub Actions / CI-CD conventions
+# docs/hardening.instructions.md — basic rules for code hardening and robustness
+# docs/securecode.instructions.md — more detailed checklists for common vulnerabilities
#
# 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
@@ -24,14 +26,27 @@ reviews:
# sequence_diagrams: false
auto_review:
enabled: true
+ base_branches:
+ - main
+ - 16_x
+ - 0_15_x
+ - V5
ignore_title_keywords:
- WIP
+ - DO NOT MERGE
+ tools:
+ fbinfer:
+ enabled: false # Arduino.h not available on Linux analysis host
+ cppcheck:
+ enabled: true # cppcheck works fine without Arduino headers
+ clang:
+ enabled: true # clang tidy likewise works
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.
+ and the general project guidelines in AGENTS.md and .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.
@@ -39,8 +54,37 @@ reviews:
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).
+
+ When reviewing PRs labeled "AI" or when source code appears to be AI-generated, perform these additional checks:
+ 1. VERIFY all referenced preprocessor macros, constants and flags exist by searching the codebase - do not trust the AI's claims about what exists.
+ 2. CHECK for reinvention: search for existing functions/patterns that already solve the same problem.
+ 3. CHECK for singleton data (defined but never used) and for dead/disabled code, and suggest to remove them.
+ 4. VERIFY comments match code behavior - AI frequently generates plausible but incorrect comments.
+ 5. VERIFY numerical stability / accuracy of arithmetic expressions. AI is often wrong when it comes to math and numbers.
+ 6. CHECK for implied but weakly justified assumptions - like usermod loop() call frequency - and ask for clarification.
+ 7. FLAG changes that appear unrelated: deleted comments, unnecessary re-formatting or re-factoring, and modifications in files that seem unrelated to the PR description.
+
+ # ── Security hardening — firmware (trust-boundary-aware) ────────────────
+ - path: "wled00/**/*.{cpp,h,hpp,ino}"
+ instructions: >
+ Apply the WLED security hardening rules from docs/hardening.instructions.md,
+ and consult docs/securecode.instructions.md when more details are needed for actionable recommendations.
+
+ Trust Boundary Model — enforce input-validation and bounds-checking rules
+ ONLY at the first untrusted ingress point. Untrusted ingress points are:
+ - HTTP/JSON API request bodies and query parameters (/json/*, /win, etc.)
+ - WebSocket message payloads
+ - UDP datagrams (parsePacket() / recvfrom() and protocol wrappers for
+ E1.31, DDP, Art-Net, TPM2.net)
+ - TCP socket reads
+ - Serial/UART command input
+ - ESP-NOW raw messages input
+
+ A value that has been validated and range-clamped at its ingress handler is
+ considered TRUSTED for all subsequent WLED core processing. Do NOT flag or suggest
+ repeated bounds/range checks or internal uses of already-sanitized data.
+ When it is unclear whether a value has been sanitized upstream, prefer
+ requesting clarification over raising a false-positive finding.
- path: "wled00/data/**"
instructions: >
@@ -49,8 +93,21 @@ reviews:
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).
+
+ # ── Security hardening — WebUI (always an ingress/output surface) ────────
+ - path: "wled00/data/**"
+ instructions: >
+ Apply the WLED web UI security rules from docs/securecode.instructions.md
+ (sections WEB1-WEB7).
+
+ The Trust Boundary Model does NOT reduce scope here: the WebUI is both
+ an ingress point (user input, postMessage, fetched config data) and an
+ output/rendering surface. Always flag DOM XSS risks, unsafe
+ innerHTML / document.write / insertAdjacentHTML / outerHTML assignments,
+ postMessage handlers without origin validation, eval() / new Function(),
+ unsafe location.href or location.replace() assignments, and DOM insertion
+ from fetched or config-derived data — regardless of where the data
+ originates.
- path: "wled00/html_*.h"
instructions: >
@@ -70,8 +127,33 @@ reviews:
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).
+
+ # ── Security hardening — usermods (trust-boundary-aware, narrow scope) ───
+ - path: "usermods/**/*.{cpp,h,hpp}"
+ instructions: >
+ For usermods, the untrusted ingress points are:
+ - readFromConfig(JsonObject& root) and calls to getJsonValue()
+ - readFromJsonState(JsonObject& obj) — JSON is parsed, but values are client-supplied
+ - onMqttMessage(char* topic, char* payload) — raw network strings, no core sanitization
+ - onEspNowMessage(uint8_t* sender, uint8_t* payload, uint8_t len) — raw radio bytes
+ - onUdpPacket(uint8_t* payload, size_t len) — raw UDP buffer, no core filtering
+ Values retrieved at these ingress points are considered trusted only after the
+ usermod itself has validated and range-clamped them.
+
+ Flag ONLY downstream uses of ingress-derived values where an out-of-range or
+ unexpected value can cause misbehaviour that is not already guarded, for example:
+ - `switch` statements on an ingress-derived value with no `default` branch,
+ or with a missing `break` where fall-through is unintentional
+ - array or buffer indexing with an ingress-derived value where the index is
+ not clamped before use
+ - arithmetic with an ingress-derived value that can overflow or produce a
+ negative result used as a size or count
+
+ Do NOT flag:
+ - getJsonValue() call sites themselves (type coercion is handled by ArduinoJson)
+ - Internal logic that operates on values already confirmed safe at ingress
+ - Repeated range checks on values that have already been clamped
+ - General memory-safety patterns unrelated to ingress-derived data flow
- path: ".github/workflows/*.{yml,yaml}"
instructions: >
@@ -83,8 +165,6 @@ reviews:
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: |
@@ -102,6 +182,73 @@ reviews:
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).
+ # ── Secrets / sensitive information scanning ────────────────────────────
+ - path: "platformio*.ini*"
+ instructions: >
+ Scan for secrets, passwords, and other sensitive information accidentally
+ committed to PlatformIO configuration files (platformio.ini,
+ platformio_override.ini, platformio_override.ini.sample).
+
+ Flag any of the following:
+ - build_flags entries that define credentials as literal values, e.g.:
+ -DWIFI_SSID=\"\" -DWIFI_PASS=\"\"
+ -DOTA_PASS=\"\" -DMQTT_PASS=\"\"
+ Flag only when the value is not a recognisable placeholder (see below).
+ - upload_flags or upload_port values that embed a password or auth token (e.g., --auth= or any URL using credential-bearing userinfo).
+ - Any key = pair whose key name contains "pass", "password",
+ "secret", "token", "key", "credential", or "auth" where the value is
+ a non-empty, non-placeholder literal string.
+ - Hardcoded IP addresses or hostnames paired with credentials in the
+ same environment section.
+ - API keys or access tokens as literal strings in any field.
+
+ Do NOT flag:
+ - Values that are clearly template placeholders (e.g., YOUR_SSID,
+ , changeme, example_token, your_password_here).
+ - Values that use PlatformIO environment variable substitution (${sysenv.WIFI_PASS} or ${env:WIFI_PASS}).
+ - Comments that only explain what a field should contain.
+ - platformio_override.ini.sample entries that contain only
+ placeholder/example values.
+
+ - path: "usermods/**/library.json"
+ instructions: >
+ Scan for secrets and sensitive information in usermod dependency manifests.
+
+ Flag any of the following:
+ - Dependency URLs that embed credentials in the URL itself (e.g., any URL containing credential-bearing userinfo).
+ - Personal access tokens, OAuth tokens, or API keys as literal strings
+ anywhere in the file.
+ - Values matching well-known secret patterns: GitHub PATs (ghp_...,
+ github_pat_...), AWS access keys (AKIA...), or similarly structured
+ high-entropy tokens.
+
+ Do NOT flag:
+ - Plain HTTPS or SSH URLs without embedded credentials.
+ - Version specifiers, semver ranges, or commit SHA references that
+ contain no credential prefix.
+ - Repository owner/name path segments (not credential material).
+
+ - path: "usermods/**/{readme,README,Readme}.md"
+ instructions: >
+ Scan for secrets, passwords, and sensitive information in usermod
+ documentation files, including inside code blocks, inline code, and prose.
+
+ Flag any of the following:
+ - Hardcoded Wi-Fi SSID or password values that appear to be real (non-placeholder)
+ strings in configuration or installation examples.
+ - Hardcoded OTA, AP, or MQTT passwords in code snippets or step-by-step
+ instructions.
+ - API keys, bearer tokens, or access tokens shown as literal values.
+ - Example platformio_override.ini snippets that contain real-looking
+ credential values instead of placeholders.
+ - Hardcoded IP addresses combined with credentials in the same example.
+
+ Do NOT flag:
+ - Values that are clearly template placeholders (e.g., YOUR_SSID,
+ , my_secret, changeme, ****).
+ - Generic prose describing what a field means without supplying a value.
+ - Asterisk-masked values (e.g., ******, ••••••).
+
finishing_touches:
# Docstrings | Options for generating Docstrings for your PRs/MRs.
docstrings:
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
index 0360a209d9..2f3de528a9 100644
--- a/.github/copilot-instructions.md
+++ b/.github/copilot-instructions.md
@@ -48,9 +48,8 @@ For detailed build timeouts, development workflows, troubleshooting, and validat
```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
+ ├── V5 # special branch: code rework for esp-idf 5.5.x and new MCU types: esp32-c5, esp32-c6, esp32-p4 (unstable)
+16_x # maintenance for release 16.x.y
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)
@@ -59,7 +58,8 @@ main # Main development trunk (daily/nightly) 17.0.0-dev
- ``main``: development trunk (daily/nightly)
-- ``V5`` and ``V5-C6``: code rework for esp-idf 5.5.x (unstable) - branched from ``main``.
+- ``V5`` : code rework for esp-idf 5.5.x (unstable) - branched from ``main``.
+- ``16_x``: maintenance for release 16.x.y
- ``0_15_x``: bugfixing / maintenance for release 0.15.x
### Repository Structure
@@ -120,20 +120,8 @@ docs/ # Contributor docs, coding guidelines
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
index cd782195de..75221948bc 100644
--- a/.github/platformio_release.ini.template
+++ b/.github/platformio_release.ini.template
@@ -35,6 +35,7 @@ default_envs = nodemcuv2
esp32s3dev_16MB_opi
esp32s3dev_8MB_opi
esp32s3dev_8MB_qspi
+ esp32s3dev_8MB_none
esp32s3_4M_qspi
; HUB75 release-only envs
esp32dev_hub75
@@ -42,3 +43,4 @@ default_envs = nodemcuv2
adafruit_matrixportal_esp32s3
esp32s3dev_16MB_opi_hub75 ;; MoonHub
esp32s3dev_4MB_qspi_hub75 ;; HD-WF2
+ waveshare_esp32s3_32MB_hub75
diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml
index 4fe55b1bd3..3737d4ff40 100644
--- a/.github/workflows/nightly.yml
+++ b/.github/workflows/nightly.yml
@@ -34,7 +34,7 @@ jobs:
# Exclude issues that were closed without resolution from changelog
excludeLabels: 'stale,wontfix,duplicate,invalid,external,question,use-as-is,not_planned'
- name: Update Nightly Release
- uses: andelf/nightly-release@main
+ uses: andelf/nightly-release@5834076edc55cc05975561c9722043f072ac5c26
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
deleted file mode 100644
index 080e70d08b..0000000000
--- a/.vscode/extensions.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- // See http://go.microsoft.com/fwlink/?LinkId=827846
- // for the documentation about the extensions.json format
- "recommendations": [
- "platformio.platformio-ide"
- ],
- "unwantedRecommendations": [
- "ms-vscode.cpptools-extension-pack"
- ]
-}
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
deleted file mode 100644
index f46f002b40..0000000000
--- a/.vscode/tasks.json
+++ /dev/null
@@ -1,42 +0,0 @@
-{
- "version": "2.0.0",
- "tasks": [
- {
- "label": "Build: HTML and binary",
- "dependsOn": [
- "Build: HTML only",
- "Build: binary only"
- ],
- "dependsOrder": "sequence",
- "problemMatcher": [
- "$platformio"
- ]
- },
- {
- "type": "PlatformIO",
- "label": "Build: binary only",
- "task": "Build",
- "group": {
- "kind": "build",
- "isDefault": true
- },
- "problemMatcher": [
- "$platformio"
- ],
- "presentation": {
- "panel": "shared"
- }
- },
- {
- "type": "npm",
- "script": "build",
- "group": "build",
- "problemMatcher": [],
- "label": "Build: HTML only",
- "detail": "npm run build",
- "presentation": {
- "panel": "shared"
- }
- }
- ]
-}
\ No newline at end of file
diff --git a/AGENTS.md b/AGENTS.md
index 733a17d7ca..9de75a715d 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -1,10 +1,13 @@
-# AGENTS.md — WLED Coding Agent Reference
+# AGENTS.md — WLED AI Coding Agent & AI Code Review Reference
WLED is C++ firmware for ESP32/ESP8266 microcontrollers controlling addressable LEDs,
with a web UI (HTML/JS/CSS). Built with PlatformIO (Arduino framework) and Node.js tooling.
See also: `.github/copilot-instructions.md`, `.github/agent-build.instructions.md`,
-`docs/cpp.instructions.md`, `docs/web.instructions.md`, `docs/cicd.instructions.md`.
+`docs/cpp.instructions.md`, `docs/web.instructions.md`, `docs/cicd.instructions.md`,
+`docs/hardening.instructions.md`, `docs/securecode.instructions.md`.
+
+Always reference these instructions - including coding guidelines in `docs/` - first and fallback to search or bash commands only when you encounter unexpected information that does not match the info here.
## Build Commands
@@ -14,8 +17,8 @@ See also: `.github/copilot-instructions.md`, `.github/agent-build.instructions.m
| `npm run build` | Build web UI into `wled00/html_*.h` / `wled00/js_*.h` | 30s |
| `npm test` | Run test suite (Node.js built-in `node --test`) | 2 min |
| `npm run dev` | Watch mode — auto-rebuilds web UI on changes | continuous |
-| `pio run -e esp32dev` | Build firmware (ESP32, most common target) | 5 min |
-| `pio run -e nodemcuv2` | Build firmware (ESP8266) | 5 min |
+| `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.
@@ -25,7 +28,7 @@ required C headers for firmware compilation.
Tests use Node.js built-in test runner (`node:test`). The single test file is
`tools/cdata-test.js`. Run it with:
-```sh
+```bash
npm test # runs all tests via `node --test`
node --test tools/cdata-test.js # run just that file directly
```
@@ -39,7 +42,7 @@ target environments. Always build after code changes: `pio run -e esp32dev`.
### Recovery / Troubleshooting
-```sh
+```bash
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
@@ -48,7 +51,7 @@ rm -rf node_modules && npm ci # reinstall Node.js deps
## Project Structure
-```
+```text
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)
@@ -61,6 +64,18 @@ docs/ # Coding convention docs
.github/workflows/ # CI/CD (GitHub Actions)
```
+### Branch / Release Structure
+
+```text
+main # Main development trunk (daily/nightly) 17.0.0-dev. Target branch for PRs.
+ ├── V5 # special branch: code rework for esp-idf 5.5.x and new MCU types: esp32-c5, esp32-c6, esp32-p4 (unstable)
+16_x # maintenance for release 16.0.x
+0_15_x # maintenance (bugfixes only) for previous release 0.15.x
+(tag) v0.14.4 # old version 0.14.4 (no maintenance)
+(tag) v0.13.3 # old version 0.13.3 (no maintenance)
+(tag) v0. ... . ... # historical versions 0.12.x and before
+```
+
## C++ Code Style (wled00/, usermods/)
### Formatting
@@ -70,6 +85,10 @@ docs/ # Coding convention docs
- Space after keywords (`if (...)`, `for (...)`), no space before function parens (`doStuff(a)`)
- No enforced line-length limit
+### Comments
+- `//` for inline (always space after), `/* */` for block comments
+- Important: AI-generated source code blocks **must be mark with `// AI: below section was generated by an AI` / `// AI: end`**
+
### Naming Conventions
| Kind | Convention | Examples |
|---|---|---|
@@ -105,16 +124,24 @@ docs/ # Coding convention docs
- No VLAs — use fixed arrays or heap allocation
- Call `reserve()` on strings/vectors to pre-allocate and avoid fragmentation
+#### ESP32 PSRAM guidelines
+
+- **Check availability**: Test chip availability with `psramFound() && ESP.getPsramSize() > 0` before assuming PSRAM is present. Never rely on `BOARD_HAS_PSRAM`only.
+- **DMA compatibility**: on ESP32 (classic), PSRAM buffers are **not DMA-capable**. On ESP32-S3 with octal PSRAM (`CONFIG_SPIRAM_MODE_OCT`), PSRAM buffers *can* be used with DMA when `CONFIG_SOC_PSRAM_DMA_CAPABLE` is defined.
+- **Fragmentation**: PSRAM allocations fragment less than DRAM because the region is larger. But avoid mixing small and large allocations in PSRAM — small allocations waste the MMU page granularity.
+- **Performance**: Prefer DRAM (or IRAM) for hot-path data that is *frequently* used. Prefer PSRAM for capacity-oriented buffers where slightly slower access times can be tolerated.
+
+Background Info:
+
+- PSRAM access is up to 18× slower than DRAM on ESP32 (dual-SPI bus), 3–10× slower than DRAM on ESP32-S3/-S2 with quad-SPI bus. On ESP32-S3 with octal PSRAM (`CONFIG_SPIRAM_MODE_OCT`), the penalty is smaller (~2×) because the 8-line DTR bus can transfer 8 bits in parallel. On ESP32-P4 with hex PSRAM (`CONFIG_SPIRAM_MODE_HEX`), the 16-line bus runs at 200 MHz which brings it on-par with DRAM.
+- Consider that ESP32 often crashes when the largest DRAM chunk gets below 10 KB.
+
### 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()`
@@ -130,6 +157,13 @@ docs/ # Coding convention docs
- `delay(1)` in custom FreeRTOS tasks (NOT `yield()`) — feeds IDLE watchdog
- Do not use `delay()` in effects (FX.cpp) or hot pixel path
+#### ESP32 Task Synchronization
+
+- Use FreeRTOS mutexes, semaphores or queues when true concurrent access from multiple FreeRTOS tasks is possible, and race-conditions can lead to unexpected behaviour.
+- **Avoid `portENTER_CRITICAL()` / `portEXIT_CRITICAL()`**, as these functions stall the complete system and may cause LEDs flickering. Prefer FreeRTOS mutexes, semaphores or queues.
+- **Important**: Not every shared resource needs a mutex. Some synchronization is guaranteed by the overall control flow, for example when function calls are sequenced within the same loop iteration.
+- Consider using `std::atomic` or RAII scoped guards as alternatives to mutexes, semaphores or queues.
+
## Web UI Code Style (wled00/data/)
- **Tab indentation** for HTML, JS, and CSS
@@ -148,17 +182,26 @@ class MyUsermod : public Usermod {
bool enabled = false;
static const char _name[];
public:
- void setup() override { /* ... */ }
- void loop() override { /* ... */ }
- void addToConfig(JsonObject& root) override { /* ... */ }
- bool readFromConfig(JsonObject& root) override { /* ... */ }
+ void setup() override { /* ... */ } // runs once at start-up
+ void loop() override { /* ... */ } // runs once per main loop iteration
+ void addToConfig(JsonObject& root) override { /* ... */ } // create/add persistent settings (usermod settings)
+ bool readFromConfig(JsonObject& root) override { /* ... */ } // read from persistent settings (usermod settings UI)
uint16_t getId() override { return USERMOD_ID_MYMOD; }
+ void addToJsonInfo(JsonObject& root) override { /* ... */ } // Add custom items to the "info" page and to /json/info
+ void appendConfigData() override { /* ... */ } // Customize the settings page: dropdowns, checkboxes, extra text, etc. Buffer size is limited!
};
const char MyUsermod::_name[] PROGMEM = "MyUsermod";
static MyUsermod myUsermod;
REGISTER_USERMOD(myUsermod);
```
+refer to detailed examples in `usermods/EXAMPLE/`, `usermods/user_fx/` and [in the user documentation for custom features](https://kno.wled.ge/advanced/custom-features/).
+
+- Activate via `custom_usermods = ` in platformio build config. The `usermod_v2_` prefix or `_v2` suffix can be omitted.
+- Base new usermods on `usermods/EXAMPLE/` (never edit the example directly)
+- Store repeated strings as `static const char[] PROGMEM`
+- Add usermod IDs to `wled00/const.h` **only when a unique ID is required** (see below)
+
### Usermod IDs
A unique ID (registered in `wled00/const.h` and overriding `getId()`) is **only required** when a usermod needs one or more of the following:
@@ -169,10 +212,13 @@ A unique ID (registered in `wled00/const.h` and overriding `getId()`) is **only
If none of the above apply, the usermod may omit `getId()` (or return the default `USERMOD_ID_UNSPECIFIED`) and does **not** need an entry in `const.h`.
-- Add usermod IDs to `wled00/const.h` **only when a unique ID is required** (see above)
-- 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`
+### Usermod `loop()`
+
+- Called once per main loop iteration. Usermods should simply `return` when `!enabled`.
+- Frequency of calls varies with system load:
+ * up to 2000 times/sec with few LEDs and little background activity,
+ * between 20 and 300 times/second during high workload from effects and other usermods,
+ * (worst case) down to 1-3 times/sec during FS activity or when serving lots of network API requests.
## CI/CD
@@ -185,12 +231,35 @@ No automated linting is configured. Match existing code style in files you edit.
## General Rules
-- Repository language is English
+- Important: Repository language is **English**. This applies to source code (including comments), commit messages and any kind of documentation for developer or users.
- The `docs/` folder is for developer/contributor information (coding conventions, architecture, etc.). User documentation is maintained in the [wled/WLED-Docs](https://github.com/wled/WLED-Docs) repository.
-- Never edit or commit auto-generated `wled00/html_*.h` / `wled00/js_*.h`
+- 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)
+- No force-push on open PRs!
+- Important: **Changes to `platformio.ini` require maintainer approval**!
+- PRs should respect `.gitignore` and not upload files like `platformio_override.ini`. PR authors may add buildenv examples for custom boards into `platformio_override.ini.sample`.
+- Remove dead/unused code — justify or delete it.
+- Verify feature-flag spelling exactly (misspellings are silently ignored by preprocessor).
- Provide references when making analyses or recommendations. Support factual claims with verifiable citations, references or concrete evidence; **never fabricate citations**.
+- **Highlight user-visible breaking changes and ripple effects** during reviews. Ask for confirmation that these were introduced intentionally.
+
+### Security Hardening
+
+When writing or reviewing code in `wled00/`, `usermods/`, `wled00/data/`, or `.github/workflows/`,
+consult `docs/hardening.instructions.md` (concise checklist) and `docs/securecode.instructions.md` (detailed rules with examples).
+These files define WLED's threat model, trust boundary model, and WLED-specific constraints (no TLS baseline, no UDP authentication for protocol-defined
+multicast/broadcast, firewall-isolated deployment assumed).
+
+### Attribution for AI-generated code
+
+Using AI-generated code can hide the source of the inspiration / knowledge / sources it used.
+
+- Document attribution of inspiration / knowledge / sources used in the code, e.g. link to GitHub repositories or other websites describing the principles / algorithms used.
+- When a larger block of code is generated by an AI tool, embed it into `// AI: below section was generated by an AI` ... `// AI: end` comments (see Comments section).
+- Every non-trivial AI-generated function should have a brief comment describing what it does. Explain parameters when their names alone are not self-explanatory.
+- AI-generated code must be well documented with meaningful comments that explain intent, assumptions, and non-obvious logic. Do not rephrase source code; explain concepts and reasoning.
+
+### 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/CONTRIBUTING.md b/CONTRIBUTING.md
index 91f1aedfe8..7edaebd29b 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -10,8 +10,8 @@ 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)
+* [Project Structure, Files and Directories](AGENTS.md#project-structure) (in our AI instructions)
+* [Instructions for creating usermods](AGENTS.md#usermod-pattern) (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.
@@ -141,8 +141,6 @@ Sometimes you might hit merge conflicts with `main` that are harder to solve. He
### Additional Resources
Want to know more? Check out:
- 📚 [GitHub Desktop documentation](https://docs.github.com/en/desktop) - if you prefer GUI tools
-- 🎓 [How to properly submit a PR](https://github.com/wled-dev/WLED/wiki/How-to-properly-submit-a-PR) - detailed tips and tricks
-
## After Approval
Once approved, a maintainer will merge your PR (possibly squashing commits).
diff --git a/docs/cpp.instructions.md b/docs/cpp.instructions.md
index 1b82d02309..8410f9147d 100644
--- a/docs/cpp.instructions.md
+++ b/docs/cpp.instructions.md
@@ -515,6 +515,8 @@ void myTask(void*) {
- **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).
+- In C/C++, additive operators (`+`, `-`) have HIGHER precedence than shift operators (`<<`, `>>`). Therefore `x - edge0 << 8` correctly parses as `(x - edge0) << 8`. Do NOT flag this pattern as a precedence bug. When reviewing WLED fixed-point code or any C/C++ shift expressions, verify against cppreference before claiming precedence issues with mixed `-`/`+` and `<<`/`>>` expressions.
+
- **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:
diff --git a/docs/esp-idf.instructions.md b/docs/esp-idf.instructions.md
index 3eff122b49..04f5891f15 100644
--- a/docs/esp-idf.instructions.md
+++ b/docs/esp-idf.instructions.md
@@ -193,7 +193,7 @@ On ESP32-S3 modules with OPI flash (e.g. N8R8 modules where the SPI flash itself
## 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.
+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` branch (`https://github.com/wled/WLED/tree/V5`). Note: WLED has not yet migrated to IDF v5 — these patterns prepare for the future migration.
### Compiler changes
@@ -295,7 +295,7 @@ The new API is channel-based:
| `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.
+**WLED impact**: NeoPixelBus LED output and IR receiver both use legacy RMT. The upstream `V5` branch adds `-D WLED_USE_SHARED_RMT` and disables IR until the library is ported.
#### I2S (Inter-IC Sound)
@@ -363,7 +363,7 @@ WLED already has a compatibility shim in `ota_update.cpp` that maps old names to
### Features disabled in IDF v5 builds
-The upstream `V5-C6` branch explicitly disables features with incompatible library dependencies:
+The upstream `V5` branch explicitly disables features with incompatible library dependencies:
```ini
# platformio.ini [esp32_idf_V5]
@@ -413,7 +413,7 @@ WLED provides convenience wrappers with automatic fallback. **Always prefer thes
### PSRAM guidelines
-- **Check availability**: always test `psramFound()` before assuming PSRAM is present.
+- **Check availability**: test availability with `psramFound() && ESP.getPsramSize() > 0` before assuming PSRAM is present. Never rely on `BOARD_HAS_PSRAM`only.
- **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
@@ -699,7 +699,7 @@ RMT drives NeoPixel LED output (via NeoPixelBus) and IR receiver input. Both use
### Migration notes
-- The upstream `V5-C6` branch uses `-D WLED_USE_SHARED_RMT` to switch to the new RMT driver for NeoPixel output.
+- The upstream `V5` 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.
diff --git a/docs/hardening.instructions.md b/docs/hardening.instructions.md
new file mode 100644
index 0000000000..3f1e53e6e3
--- /dev/null
+++ b/docs/hardening.instructions.md
@@ -0,0 +1,58 @@
+---
+applyTo: "**/*.{cpp,h,hpp,ino,js,htm,html,css,yml,yaml}"
+description: "WLED strict-mode security review: low-noise checklist."
+---
+
+# WLED Security Review — Low Noise Mode
+
+Use these code hardening rules for automated reviews with minimal false positives.
+
+## WLED Constraints (apply to all rules)
+
+- Assume firewall/DMZ/VPN deployment; focus on realistic LAN-local and supply-chain risks.
+- Do **not** require TLS/HTTPS as a baseline control.
+- Do **not** require authentication for standards-based UDP multicast/broadcast paths where authentication is not defined in the protocol specification.
+
+> **Trust boundary model**: Apply input-validation rules **only at the first untrusted ingress point**
+> (HTTP/JSON API body or query string, WebSocket payload, UDP datagram, TCP read, serial command, ESP-NOW raw messages).
+> Values that have been validated and range-clamped at ingress are **trusted** for internal WLED
+> processing. Do not flag subsequent uses or internal copies of already-sanitized data.
+
+## CRITICAL Rules
+
+1. **No unchecked buffer copies** (`memcpy`, `memmove`, `strcpy`) in firmware paths when source buffer or size comes from an untrusted origin; prefer bounded alternatives (`strncpy`, `strlcpy`); require length validation before copying.
+2. **No user-controlled format strings** in `DEBUG_PRINTF*` and similar logging APIs.
+3. **Validate all untrusted external input** (HTTP/JSON/UDP/serial) before index/length/pin usage.
+4. **Auth required for state-changing control endpoints where feasible** (for example HTTP/JSON); do not flag protocol-defined unauthenticated UDP multicast/broadcast channels solely for missing auth.
+5. **No fail-open on parse/allocation errors** for config/state updates.
+6. **No DOM XSS sinks with untrusted data** (`innerHTML`, unsafe HTML insertion). Server-side generation of JavaScript property-assignment statements (as used in WLED's printSetForm* helpers) is exempt.
+7. **No dynamic code execution** (`eval`, `new Function`, string timers).
+8. **No hardcoded secrets/credentials/tokens/keys** in committed files.
+9. **No sensitive data in logs** (passwords, tokens, Wi-Fi secrets, auth headers).
+10. **No secret exposure in workflows/log output, or in LittleFS files other than `wsec.json`**.
+11. **No unsafe third-party GitHub Action pinning** (`@main`/`@master` disallowed).
+12. **No untrusted expression interpolation in workflow shell commands**.
+
+## IMPORTANT Rules
+
+13. Avoid potentially unbounded string/memory operations (`strcmp`, `strchr`, `strlen`, `sprintf`) in firmware paths; prefer bounded alternatives (`strnlen`, `strncmp`, `snprintf`).
+14. Check integer overflow risks in size/index arithmetic, but consider that unsigned wrap-around on small types might be intentional.
+15. Reject repeated heap allocation churn in hot render/effect loops.
+16. Avoid repeated `String` growth in hot paths; prefer bounded/pre-allocated buffers.
+17. Ensure UI validation is mirrored by firmware-side validation.
+18. Require strict origin checks for `postMessage` listeners.
+19. Disallow untrusted redirect/navigation targets.
+20. Prevent verbose error responses that leak internals.
+21. Review new dependencies for typosquatting and known vulnerability risk.
+22. Keep workflow `permissions` least-privilege.
+23. Verify new `WLED_ENABLE_*` / `WLED_DISABLE_*` names are valid known flags.
+24. New privileged behavior must not be enabled by insecure defaults; first-use default-credential change required where applicable.
+25. OTA paths (Update.begin(), Update.write()) must verify firmware integrity (checksum/hash); TLS not required.
+26. Flag xTaskCreate/xTaskCreatePinnedToCore tasks with insufficient stack for String/JSON use; flag MDNS.begin() / ArduinoOTA.setHostname() with unsanitized hostnames.
+27. Flag API/config serialization that exposes Wi-Fi/AP/MQTT password fields to unauthenticated clients.
+28. Treat fetched and config-derived strings as untrusted when inserting into the DOM; explicit sanitization required for HTML contexts.
+
+## Reviewer Output Format
+
+- Include severity, exact file and line, and one concrete fix direction.
+- Prioritize CRITICAL findings before IMPORTANT findings.
diff --git a/docs/securecode.instructions.md b/docs/securecode.instructions.md
new file mode 100644
index 0000000000..2e71da6beb
--- /dev/null
+++ b/docs/securecode.instructions.md
@@ -0,0 +1,227 @@
+---
+applyTo: "**/*.{cpp,h,hpp,ino,js,htm,html,css,yml,yaml}"
+description: "WLED-focused security review guide based on OWASP Top 10 for embedded firmware and web UI."
+---
+
+# WLED Security Review Standards (Embedded + Web UI)
+
+Use this guide for AI-assisted code reviews in:
+- `wled00/`
+- `usermods/`
+- `.github/workflows/`
+
+## WLED Constraints and Threat Model Assumptions
+
+- Assume typical deployment behind a firewall/DMZ/VPN; prioritize LAN-local and supply-chain risks.
+- Do **not** require TLS/HTTPS as a baseline control for findings in this repo.
+- Do **not** require authentication for standards-based UDP multicast/broadcast protocols where auth is not part of the spec.
+- Do not propose mitigations that break protocol compliance just to add authentication.
+
+### Trust Boundary Model
+
+**Untrusted data** enters WLED only at the following explicit ingress points:
+- HTTP/JSON API request bodies and query parameters (e.g., `/json/*`, `/win`)
+- WebSocket message payloads
+- UDP datagrams (`parsePacket()` / `recvfrom()` and higher-level protocol wrappers)
+- TCP socket reads
+- Serial/UART input used as commands
+- ESP-NOW raw messages input
+
+**Validation and range-clamping applied at the ingress point renders data trusted** for all subsequent use within the WLED core.
+
+**Do not flag:**
+- Repeated bounds or range checks on a value that has already been validated and clamped at its ingress handler.
+- Internal WLED core logic that operates on values confirmed safe by the ingress layer.
+
+If it is unclear whether a value has been sanitized upstream (e.g., passed through multiple function calls without a clear annotation), prefer asking for clarification over raising a false-positive finding.
+
+### Locally-Stored Configuration Files (Robustness, not a primary trust boundary)
+
+Files read from LittleFS (`presets.json`, `cfg.json`, `ledmap.json`, `ir.json`, etc.) are written only via privileged access (`/edit`) and are considered trusted in the threat model.
+However, parse them defensively (validate structure, clamp array sizes, handle missing keys gracefully) to avoid bootloops from filesystem corruption or accidental malformation.
+
+## Severity
+
+- **CRITICAL** — exploitable vulnerability; block merge.
+- **IMPORTANT** — meaningful risk; fix before or with merge when practical.
+- **SUGGESTION** — defense-in-depth; track for follow-up.
+
+## Scope (WLED-relevant)
+
+Prioritize:
+- C++ memory safety and input validation
+- Auth and access checks for state-changing HTTP/JSON APIs
+- XSS and DOM safety in `wled00/data/*`
+- Secrets handling (`wsec.json`) and secure logging
+- Dependency and GitHub Actions supply-chain hygiene
+- Fail-safe behavior on constrained devices
+
+De-prioritize unless explicitly introduced by a PR:
+- SQL/NoSQL checks, JWT/OAuth flows, GraphQL-specific checks, generic backend framework checks not used by WLED.
+
+## Firmware Security (C++, OWASP A01/A04/A05/A10)
+
+### FW1: Unsafe buffer operations
+- **Severity**: CRITICAL
+- Flag `strcpy`, `sprintf`, unchecked memory access (`memcpy`, `memmove`, `memcmp`, `strcmp`, `strlen`), unchecked pointer arithmetic.
+- Require explicit bounds checks and length validation.
+- Prefer bounded alternatives for string operations (`strnlen`, `strncmp`, `strncpy`, `strlcpy`, `snprintf`).
+- Treat a finding against FW1 as **suggestion** only when the operation is provably bounded
+ and both the destination capacity and copied/compared length are known safe.
+
+### FW2: Format-string injection
+- **Severity**: CRITICAL
+- Do not pass untrusted input as a format string to `DEBUG_PRINTF*` or similar APIs.
+
+### FW3: Integer overflow in length and offset math
+- **Severity**: IMPORTANT
+- Review `count * size`, index math, narrowing casts before allocations or copies.
+
+### FW4: Unvalidated external input
+- **Severity**: CRITICAL
+- At each **untrusted ingress point** (see Trust Boundary Model above), validate and clamp values from HTTP/JSON/UDP/serial before use as lengths, indices, IDs, or pin references.
+- Do not flag repeated range checks on values that have already been validated at their ingress point.
+- In UDP handlers (`parsePacket()`, `read()`, and any lower-level socket wrappers), validate `packetSize` before buffer writes and clamp protocol-specific universe/channel ranges to valid limits.
+
+### FW5: Missing auth checks on state-changing endpoints (where auth is feasible)
+- **Severity**: CRITICAL
+- HTTP/JSON and other control paths that support auth must enforce configured auth policy.
+- Do not flag the HTTP endpoint `/reset` as state-changing. This endpoint triggers a reboot, causing a short interruption without loss of user data.
+- Do not flag standards-based UDP multicast/broadcast paths solely for lacking authentication when authentication is not defined in the protocol specification.
+
+### FW6: Fail-open behavior after parse or allocation errors
+- **Severity**: IMPORTANT
+- On error, reject update and preserve safe previous state.
+- Explicitly check parse status (`DeserializationError error = deserializeJson(...); if (error) return/reject;`) and avoid silently applying unsafe zero/default values to safety-relevant fields (for example LED count and pin assignment).
+
+### FW7: Heap churn in hot paths
+- **Severity**: IMPORTANT
+- Avoid repeated dynamic allocation in render/effect loops; prefer pre-allocation and reuse.
+- Flag allocation patterns in loop and ISR-adjacent paths that can trigger fragmentation or timing instability.
+
+### FW8: Unsafe use of `String` in performance-critical paths
+- **Severity**: IMPORTANT
+- In hot paths, avoid repeated `String` growth; reserve or use fixed buffers.
+- Flag repeated `String` concatenation inside loop-heavy or ISR-adjacent code.
+
+### FW9: Unsafe feature flag names
+- **Severity**: IMPORTANT
+- Verify all new `WLED_ENABLE_*`/`WLED_DISABLE_*` names are valid known flags; typos silently alter build behavior.
+
+### FW10: OTA integrity verification (without TLS requirement)
+- **Severity**: IMPORTANT
+- OTA update flows should validate firmware integrity using the checksum/hash/signature mechanism available in the firmware/platform implementation.
+- Do not require TLS/certificate pinning as a mandatory review criterion.
+- In OTA paths (`Update.begin()`, `Update.write()`, and related flows), flag flashing without integrity verification.
+
+### FW11: FreeRTOS task stack and recursion safety
+- **Severity**: IMPORTANT
+- In `xTaskCreate`/`xTaskCreatePinnedToCore` tasks that process `String`/JSON-heavy data, verify stack-size sufficiency and avoid unbounded recursion.
+
+### FW12: mDNS and hostname sanitization
+- **Severity**: IMPORTANT
+- For `MDNS.begin()`, `MDNS.addService()`, and `ArduinoOTA.setHostname()`, ensure user-provided hostnames are RFC-compliant (letters/digits/hyphen, no leading/trailing hyphen) and clamped to 63 characters.
+
+### FW13: Outbound URL validation (no HTTPS requirement)
+- **Severity**: SUGGESTION
+- When using user-provided URL strings with `HTTPClient.begin()`/equivalent, validate scheme/format and constrain host targets (allowlist or equivalent policy).
+- Do not require HTTPS/TLS as a baseline review rule.
+
+### FW14: Optional unicast UDP source filtering
+- **Severity**: SUGGESTION
+- For unicast UDP receive paths, prefer optional user-configurable source filtering.
+- Do not require this for multicast/broadcast protocol flows.
+
+## Web UI Security (`wled00/data/*`, OWASP A01/A02/A05)
+
+### WEB1: DOM XSS through `innerHTML`
+- **Severity**: CRITICAL
+- Prefer `textContent`; if HTML is required, sanitize trusted content path explicitly.
+
+### WEB2: Dynamic code execution
+- **Severity**: CRITICAL
+- Reject `eval`, `new Function`, and string-based timer execution.
+
+### WEB3: `postMessage` without origin validation
+- **Severity**: IMPORTANT
+- Require strict origin allowlist checks before processing payloads.
+
+### WEB4: Unsafe redirects/navigation
+- **Severity**: IMPORTANT
+- Do not navigate directly from untrusted query/input without relative-path or allowlist checks.
+
+### WEB5: Client-only validation
+- **Severity**: IMPORTANT
+- UI validation is not sufficient; equivalent firmware-side validation is required.
+
+### WEB6: Direct DOM insertion from fetched/config data
+- **Severity**: IMPORTANT
+- Treat fetched and config-derived strings as untrusted unless proven otherwise.
+
+### WEB7: CSRF checks for state-changing HTTP routes (advisory)
+- **Severity**: SUGGESTION
+- For state-changing HTTP routes (for example `/json/state`, `/win`), prefer `Origin`/`Referer` header validation as low-cost defense-in-depth for deployments that are not directly internet-exposed.
+- Treat this as advisory only, since some legitimate clients may omit these headers.
+
+## Secrets and Logging (OWASP A04/A09/A10)
+
+### SEC1: Hardcoded secrets and credentials
+- **Severity**: CRITICAL
+- Reject committed API keys, passwords, tokens, private keys, or test backdoors with potential security impact.
+
+### SEC2: Sensitive values in logs
+- **Severity**: CRITICAL
+- Do not log passwords, tokens, Wi-Fi keys, auth headers, or full sensitive payloads.
+
+### SEC3: Insecure defaults
+- **Severity**: IMPORTANT
+- Reject new default credentials or insecure auto-enable behavior for privileged functions.
+- For setup/onboarding flows, require first-change behavior for default credentials where applicable.
+
+### SEC4: Overly detailed error responses
+- **Severity**: IMPORTANT
+- Avoid exposing stack traces or internal details to API/UI consumers.
+
+### SEC5: Credential exposure in API/config responses
+- **Severity**: IMPORTANT
+- Flag API/config serialization that exposes password-like fields (for example Wi-Fi/AP/MQTT passwords) to unauthenticated or untrusted clients.
+
+### SEC6: Security-relevant event logging coverage
+- **Severity**: SUGGESTION
+- Prefer explicit logging for auth failures, OTA attempts, config resets, and AP activation events, without logging secret values.
+
+## Supply Chain and CI/CD (OWASP A03/A08)
+
+### SC1: New dependency risk
+- **Severity**: IMPORTANT
+- Review new npm/pip/PlatformIO dependencies for legitimacy, pinning, and known vulnerabilities.
+
+### SC2: Workflow permission hardening regressions
+- **Severity**: IMPORTANT
+- Check for broad `permissions`, unpinned third-party actions, or unsafe secret exposure.
+- Flag mutable third-party action refs (`@main`, `@master`, broad tags) where SHA pinning is expected by project policy.
+- Flag overly broad permissions such as `write-all` without clear need.
+
+### SC3: Script injection in workflows
+- **Severity**: IMPORTANT
+- Avoid direct interpolation of untrusted `${{ github.event.* }}` values in `run` commands.
+
+## Reviewer Checklist
+
+- [ ] No new memory-safety hazards (bounds, overflow, unsafe copies/format strings)
+- [ ] External input is validated and range-clamped at ingress points (HTTP/JSON, WebSocket, UDP, TCP, serial, ESP-NOW)
+- [ ] State-changing API paths enforce auth policy
+- [ ] OTA paths enforce integrity verification (without requiring TLS baseline)
+- [ ] Suggested rule patterns are checked where relevant (UDP bounds, hostname sanitization, workflow pinning/permissions)
+- [ ] Web UI changes avoid unsafe DOM execution/injection patterns
+- [ ] No secrets added; no sensitive logging introduced
+- [ ] Error handling remains fail-safe and non-leaky
+- [ ] Dependency/workflow changes are supply-chain safe
+- [ ] Feature-flag names are valid and not typoed
+
+## AI Review Behavior
+
+- Prefer concrete, file/line-specific findings over generic guidance.
+- Prioritize **CRITICAL** and **IMPORTANT** findings.
+- Skip irrelevant framework checks not used by WLED.
+- If control-flow trust is unclear, ask for clarification instead of guessing.
diff --git a/docs/web.instructions.md b/docs/web.instructions.md
index 87c9b74531..1e4b621907 100644
--- a/docs/web.instructions.md
+++ b/docs/web.instructions.md
@@ -23,6 +23,12 @@ applyTo: "wled00/data/**"
**Reuse shared helpers from `common.js` whenever possible** instead of duplicating utilities in page-local scripts.
+## Accessibility & Interaction
+
+The WLED web UI targets commonly used browser/platform combinations: desktop browsers on Mac and PC (primarily pointer-driven, touch rare),
+and touch-only devices (phones, tablets). If possible, keep the UI accessible to users with disabilities.
+Full keyboard operability is not a strict requirement - adding keyboard shortcuts should be a case-by-case decision.
+
## Build Integration
Files in this directory are processed by `tools/cdata.js` into generated headers
diff --git a/package-lock.json b/package-lock.json
index 8d9e3231ce..98d7e70fac 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "wled",
- "version": "17.0.0-dev",
+ "version": "16.0.1-dev",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "wled",
- "version": "17.0.0-dev",
+ "version": "16.0.1-dev",
"license": "ISC",
"dependencies": {
"clean-css": "^5.3.3",
diff --git a/package.json b/package.json
index 58016850cf..c3082a460a 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "wled",
- "version": "17.0.0-dev",
+ "version": "16.0.1-dev",
"description": "Tools for WLED project",
"main": "tools/cdata.js",
"directories": {
diff --git a/pio-scripts/dynarray.py b/pio-scripts/dynarray.py
index 2e9d383598..7cfd0bd579 100644
--- a/pio-scripts/dynarray.py
+++ b/pio-scripts/dynarray.py
@@ -19,13 +19,7 @@
def inject_before_marker(path, marker):
"""Patch a linker script file in-place, inserting DYNARRAY_INJECTION before marker."""
original = path.read_text()
- marker_pos = original.find(marker)
- if marker_pos < 0:
- raise RuntimeError(
- f"DYNARRAY injection marker not found in linker script: path={path}, marker={marker!r}"
- )
- patched = original[:marker_pos] + DYNARRAY_INJECTION + original[marker_pos:]
- path.write_text(patched)
+ path.write_text(original.replace(marker, DYNARRAY_INJECTION + marker, 1))
if env.get("PIOPLATFORM") == "espressif32":
@@ -45,6 +39,7 @@ def inject_before_marker(path, marker):
# 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(.);")
diff --git a/pio-scripts/validate_modules.py b/pio-scripts/validate_modules.py
index 0788be93d7..3471a8d151 100644
--- a/pio-scripts/validate_modules.py
+++ b/pio-scripts/validate_modules.py
@@ -37,14 +37,14 @@ def check_elf_modules(elf_path: Path, env, module_lib_builders) -> set[str]:
Returns the set of build_dir basenames for confirmed modules.
"""
readelf_path = _get_readelf_path(env)
+ secho(f"INFO: Checking for usermod compilation units...")
+
try:
result = subprocess.run(
[readelf_path, "--debug-dump=info", "--dwarf-depth=1", str(elf_path)],
capture_output=True, text=True, errors="ignore", timeout=120,
)
output = result.stdout
- if result.returncode != 0 or result.stderr.strip():
- secho(f"WARNING: readelf exited {result.returncode}: {result.stderr.strip()}", fg="yellow", err=True)
except (subprocess.TimeoutExpired, FileNotFoundError, OSError) as e:
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
diff --git a/platformio.ini b/platformio.ini
index 5ff4d9028c..b0e5451c28 100644
--- a/platformio.ini
+++ b/platformio.ini
@@ -30,6 +30,7 @@ default_envs = nodemcuv2
esp32s3dev_16MB_opi
esp32s3dev_8MB_opi
esp32s3dev_8MB_qspi
+ esp32s3dev_8MB_none
esp32s3_4M_qspi
usermods
@@ -129,7 +130,7 @@ upload_speed = 115200
lib_compat_mode = strict
lib_deps =
IRremoteESP8266 @ 2.8.2
- https://github.com/Makuna/NeoPixelBus.git#a0919d1c10696614625978dd6fb750a1317a14ce
+ https://github.com/Makuna/NeoPixelBus.git#1d7ff38f14d04a976f9c6e365c83a232bcba04fc
https://github.com/Aircoookie/ESPAsyncWebServer.git#v2.4.2
marvinroger/AsyncMqttClient @ 0.9.0
# for I2C interface
@@ -195,6 +196,7 @@ platform_packages = platformio/toolchain-xtensa @ ~2.100300.220621 #2.40802.2005
platform = ${esp8266.platform_wled_default}
build_unflags = ${common.build_unflags}
+custom_usermods =
build_flags =
-DESP8266
-DFP_IN_IROM
@@ -297,8 +299,18 @@ AR_lib_deps = ;; for pre-usermod-library platformio_override compatibility
;; please note that you can NOT update existing ESP32 installs with a "V4" build. Also updating by OTA will not work properly.
;; You need to completely erase your device (esptool erase_flash) first, then install the "V4" build from VSCode+platformio.
+;; tasmota platform (default)
platform = https://github.com/tasmota/platform-espressif32/releases/download/2024.06.00/platform-espressif32.zip ;; Tasmota Arduino Core 2.0.18 with IPv6 support, based on IDF 4.4.8
platform_packages =
+
+;; espressif platform (optional - needs 300KB extra flash)
+;;; arduino-esp32 2.0.17 + esp-idf 4.4.7
+;; platform = espressif32@ ~6.13.0
+;; platform_packages =
+;;; arduino-esp32 2.0.14 + esp-idf 4.4.6
+;; platform = espressif32@ ~6.6.0
+;; platform_packages = platformio/framework-arduinoespressif32 @ 3.20014.231204
+
build_unflags = ${common.build_unflags}
build_flags = -g
-Wshadow=compatible-local ;; emit warning in case a local variable "shadows" another local one
@@ -382,6 +394,8 @@ board = nodemcuv2
board_build.ldscript = ${common.ldscript_4m1m}
build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP8266\" #-DWLED_DISABLE_2D
-D WLED_DISABLE_PARTICLESYSTEM2D
+lib_deps = ${esp8266.lib_deps}
+monitor_filters = esp8266_exception_decoder
custom_usermods = audioreactive
[env:nodemcuv2_compat]
@@ -408,6 +422,7 @@ board_build.ldscript = ${common.ldscript_2m512k}
build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP02\"
-D WLED_DISABLE_PARTICLESYSTEM2D
-D WLED_DISABLE_PARTICLESYSTEM1D
+lib_deps = ${esp8266.lib_deps}
custom_usermods = audioreactive
[env:esp8266_2m_compat]
@@ -432,7 +447,7 @@ custom_usermods = audioreactive
;; Minimal-feature build for ESP02 (2MB flash).
;; Use this to recover from a failed OTA: flash via serial, then OTA-upload the regular esp8266_2m binary.
;; OTA is intentionally kept enabled. All other optional features are stripped to minimise binary size.
-extends = esp8266
+extends = env:esp8266_2m
board = esp_wroom_02
board_build.ldscript = ${common.ldscript_2m512k}
build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP02_min\"
@@ -478,7 +493,7 @@ build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=
-D WLED_DISABLE_PARTICLESYSTEM1D
-D WLED_DISABLE_PARTICLESYSTEM2D
-D WLED_DISABLE_PIXELFORGE
-custom_usermods = audioreactive
+;; custom_usermods = audioreactive ;; pushed program flash size over the limits
[env:esp32dev]
extends = esp32
@@ -599,6 +614,19 @@ build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=
-DBOARD_HAS_PSRAM
;; -DLOLIN_WIFI_FIX ;; uncomment if you have WiFi connectivity problems
+[env:esp32s3dev_8MB_none]
+;; ESP32-S3 development board, 8MB FLASH, no PSRAM
+extends = esp32s3
+board = esp32-s3-devkitc-1 ;; generic dev board
+custom_usermods = audioreactive
+build_unflags = ${esp32s3.build_unflags} -DBOARD_HAS_PSRAM ;; make sure PSRAM support is removed
+build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_8MB_none\"
+ -D WLED_WATCHDOG_TIMEOUT=0
+ ;-D ARDUINO_USB_CDC_ON_BOOT=0 ;; -D ARDUINO_USB_MODE=1 ;; for boards with serial-to-USB chip
+ -D ARDUINO_USB_CDC_ON_BOOT=1 ;; -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB")
+board_build.f_flash = 80000000L
+board_build.flash_mode = qio
+
[env:esp32S3_wroom2]
;; For ESP32-S3 WROOM-2, a.k.a. ESP32-S3 DevKitC-1 v1.1
;; with >= 16MB FLASH and >= 8MB PSRAM (memory_type: opi_opi)
@@ -649,6 +677,21 @@ board_build.partitions = ${esp32.default_partitions}
board_build.f_flash = 80000000L
board_build.flash_mode = qio
+[env:esp32s3_4M_none]
+;; ESP32-S3 with 4MB FLASH, no PSRAM
+extends = esp32s3
+board = esp32-s3-devkitc-1
+custom_usermods = audioreactive
+build_unflags = ${esp32s3.build_unflags} -D BOARD_HAS_PSRAM ;; make sure PSRAM support is removed
+build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_4M_none\"
+ -DARDUINO_USB_CDC_ON_BOOT=1 ;; -DARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB")
+ -DLOLIN_WIFI_FIX ; seems to work much better with this
+ -D WLED_WATCHDOG_TIMEOUT=0
+board_build.partitions = ${esp32.default_partitions}
+board_build.f_flash = 80000000L
+board_build.flash_mode = qio
+
+
[env:lolin_s2_mini]
extends = esp32s2
board = lolin_s2_mini
@@ -674,7 +717,10 @@ build_flags = ${common.build_flags} ${esp32s2.build_flags} -D WLED_RELEASE_NAME=
[env:usermods]
extends = env:esp32dev
build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=\"ESP32_USERMODS\"
- -DTOUCH_CS=9
+ -DTOUCH_CS=9 -DWLED_USE_SD_SPI ;; help a few usermods that require special flags to compile
+lib_deps = ${esp32_idf_V4.lib_deps}
+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
@@ -691,6 +737,7 @@ board_build.partitions = ${esp32.extreme_partitions} ; We're gonna need a bigge
;; Core HUB75 flags - common to every HUB75 build
build_flags =
-D WLED_ENABLE_HUB75MATRIX -D NO_GFX
+ -D NO_CIE1931 ;; disable driver-internal gamma correction
-D WLED_DEBUG_BUS
-D LED_TYPES=TYPE_HUB75MATRIX_HS
; -D WLED_DEBUG
@@ -731,8 +778,8 @@ build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} ${hub75.build_fl
; 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
+board_build.partitions = ${esp32.extended_partitions} ;; 1.65MB firmware, 700KB filesystem - only 4MB flash usable on this board
+build_unflags = ${esp32s3.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
@@ -750,6 +797,28 @@ lib_deps = ${esp32s3.lib_deps}
${hub75.lib_deps}
;; board_build.partitions = tools/partitions-8MB_spiffs-tinyuf2.csv ;; supports adafruit UF2 bootloader
+[env:waveshare_esp32s3_32MB_hub75]
+;; Waveshare ESP32-S3-RGB-Matrix (memory_type: opi_opi); see https://docs.waveshare.com/ESP32-S3-RGB-Matrix
+extends = env:esp32S3_wroom2_32MB
+monitor_filters = esp32_exception_decoder
+build_unflags = ${env:esp32S3_wroom2_32MB.build_unflags}
+ -D WLED_RELEASE_NAME=\"ESP32-S3_WROOM-2_32MB\" ;; need to un-set the relese name before setting a new one
+custom_usermods =
+ Internal_Temperature
+ audioreactive = https://github.com/MoonModules/WLED-AudioReactive-Usermod#171c0bbc2bc47e2c11ae203dd00beed82865df2b ;; broadcast
+build_flags = ${common.build_flags} ${esp32s3.build_flags} ${hub75.build_flags} ${hub75.s3_build_flags}
+ -D WLED_RELEASE_NAME=\"ESP32-S3_Waveshare_HUB75\"
+ -D WAVESHARE_S3_PINOUT
+ -D BTNPIN=0
+ -D I2CSDAPIN=47 -D I2CSCLPIN=48
+ -D SR_DMTYPE=9 -D I2S_SDPIN=39 -D I2S_CKPIN=43 -D I2S_WSPIN=38 -D MCLK_PIN=12 ; Waveshare ES8311 Codec pins
+ -D UM_SD_SELECT=14 -D UM_SD_CLOCK=1 -D UM_SD_POCI=17 -D UM_SD_PICO=44 ; Waveshare SD Card pins ** Requires updated sd_card from this PR https://github.com/wled/WLED/pull/5679 **
+ -D WLED_USE_SD_SPI
+ -D SD_PRINT_HOME_DIR
+; -D WLED_DEBUG
+lib_deps = ${esp32s3.lib_deps}
+ ${hub75.lib_deps}
+
[env:esp32s3dev_16MB_opi_hub75]
;; MOONHUB HUB75 adapter board (lilygo T7-S3 with 16MB flash and octal PSRAM)
extends = env:esp32s3dev_8MB_opi
diff --git a/platformio_override.sample.ini b/platformio_override.sample.ini
index 16e73be007..6dbf5d1009 100644
--- a/platformio_override.sample.ini
+++ b/platformio_override.sample.ini
@@ -5,7 +5,7 @@
# Please visit documentation: https://docs.platformio.org/page/projectconf.html
[platformio]
-default_envs = WLED_generic8266_1M, esp32dev_V4_dio80 # put the name(s) of your own build environment here. You can define as many as you need
+default_envs = WLED_generic8266_1M, esp32dev_dio80 # put the name(s) of your own build environment here. You can define as many as you need
#----------
# SAMPLE
@@ -30,7 +30,10 @@ lib_deps = ${esp8266.lib_deps}
; https://github.com/blazoncek/QuickESPNow.git#optional-debug ;; exludes debug library
build_unflags = ${common.build_unflags}
-build_flags = ${common.build_flags} ${esp8266.build_flags}
+build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP8266_generic_1M\"
+ -D WLED_DISABLE_PARTICLESYSTEM1D -D WLED_DISABLE_PARTICLESYSTEM2D
+ -D WLED_DISABLE_PIXELFORGE
+ -D WLED_DISABLE_OTA -D WLED_DISABLE_2D
;
; *** To use the below defines/overrides, copy and paste each onto its own line just below build_flags in the section above.
;
@@ -184,6 +187,7 @@ build_flags = ${common.build_flags} ${esp8266.build_flags}
; configure I2C and SPI interface (for various hardware)
; -D I2CSDAPIN=33 # initialise interface
; -D I2CSCLPIN=35 # initialise interface
+; # HW_PIN_* informs the WebUI about default pins - don't initialise interface
; -D HW_PIN_SCL=35
; -D HW_PIN_SDA=33
; -D HW_PIN_CLOCKSPI=7
@@ -219,7 +223,8 @@ platform = ${esp8266.platform_wled_default}
platform_packages = ${esp8266.platform_packages}
board_build.ldscript = ${common.ldscript_4m1m}
build_unflags = ${common.build_unflags}
-build_flags = ${common.build_flags} ${esp8266.build_flags}
+build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP8266_ESP07\" #-DWLED_DISABLE_2D
+ -D WLED_DISABLE_PARTICLESYSTEM2D
lib_deps = ${esp8266.lib_deps}
[env:d1_mini]
@@ -229,7 +234,8 @@ platform_packages = ${esp8266.platform_packages}
upload_speed = 921600
board_build.ldscript = ${common.ldscript_4m1m}
build_unflags = ${common.build_unflags}
-build_flags = ${common.build_flags} ${esp8266.build_flags}
+build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP8266_D1MINI\" #-DWLED_DISABLE_2D
+ -D WLED_DISABLE_PARTICLESYSTEM2D
lib_deps = ${esp8266.lib_deps}
monitor_filters = esp8266_exception_decoder
@@ -239,7 +245,8 @@ platform = ${esp8266.platform_wled_default}
platform_packages = ${esp8266.platform_packages}
board_build.ldscript = ${common.ldscript_4m1m}
build_unflags = ${common.build_unflags}
-build_flags = ${common.build_flags} ${esp8266.build_flags}
+build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP8266_HT_D1MINI\" #-DWLED_DISABLE_2D
+ -D WLED_DISABLE_PARTICLESYSTEM2D
lib_deps = ${esp8266.lib_deps}
[env:h803wf]
@@ -248,30 +255,24 @@ platform = ${esp8266.platform_wled_default}
platform_packages = ${esp8266.platform_packages}
board_build.ldscript = ${common.ldscript_4m1m}
build_unflags = ${common.build_unflags}
-build_flags = ${common.build_flags} ${esp8266.build_flags} -D DATA_PINS=1 -D WLED_DISABLE_INFRARED
+build_flags = ${common.build_flags} ${esp8266.build_flags} -D DATA_PINS=1 -D WLED_DISABLE_INFRARED -D WLED_RELEASE_NAME=\"ESP8266_HT803WF\" #-DWLED_DISABLE_2D
+ -D WLED_DISABLE_PARTICLESYSTEM2D
lib_deps = ${esp8266.lib_deps}
[env:esp32dev_qio80]
extends = env:esp32dev # we want to extend the existing esp32dev environment (and define only updated options)
board = esp32dev
-build_flags = ${common.build_flags} ${esp32.build_flags} #-D WLED_DISABLE_BROWNOUT_DET
-lib_deps = ${esp32.lib_deps}
+build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=\"ESP32_qio80\" #-D WLED_DISABLE_BROWNOUT_DET
+lib_deps = ${esp32_idf_V4.lib_deps}
monitor_filters = esp32_exception_decoder
board_build.f_flash = 80000000L
board_build.flash_mode = qio
-[env:esp32dev_V4_dio80]
-;; experimental ESP32 env using ESP-IDF V4.4.x
-;; Warning: this build environment is not stable!!
-;; please erase your device before installing.
-extends = esp32_idf_V4 # based on newer "esp-idf V4" platform environment
-board = esp32dev
-build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} #-D WLED_DISABLE_BROWNOUT_DET
-lib_deps = ${esp32_idf_V4.lib_deps}
-monitor_filters = esp32_exception_decoder
-board_build.partitions = ${esp32.default_partitions} ;; if you get errors about "out of program space", change this to ${esp32.extended_partitions} or even ${esp32.big_partitions}
-board_build.f_flash = 80000000L
-board_build.flash_mode = dio
+[env:esp32dev_dio80]
+extends = env:esp32dev_qio80 # we want to extend the previous environment, to change flash speed
+build_unflags = ${env:esp32dev_qio80.build_unflags} -D WLED_RELEASE_NAME=\"ESP32_qio80\" # need to remove the previous WLED_RELEASE_NAME
+build_flags = ${env:esp32dev_qio80.build_flags} -D WLED_RELEASE_NAME=\"ESP32_dio80\" # ... and then we can set a new one
+board_build.flash_mode = dio # change flash mode to "dio", for boards that cannot not start with "qio" mode
[env:esp32s2_saola]
extends = esp32s2
@@ -281,25 +282,19 @@ platform_packages = ${esp32s2.platform_packages}
framework = arduino
board_build.flash_mode = qio
upload_speed = 460800
-build_flags = ${common.build_flags} ${esp32s2.build_flags}
+build_flags = ${common.build_flags} ${esp32s2.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S2_saola\"
;-DLOLIN_WIFI_FIX ;; try this in case Wifi does not work
-DARDUINO_USB_CDC_ON_BOOT=1
lib_deps = ${esp32s2.lib_deps}
-[env:esp32s3dev_8MB_PSRAM_qspi]
-;; ESP32-TinyS3 development board, with 8MB FLASH and PSRAM (memory_type: qio_qspi)
-extends = env:esp32s3dev_8MB_PSRAM_opi
-;board = um_tinys3 ; -> needs workaround from https://github.com/wled-dev/WLED/pull/2905#issuecomment-1328049860
-board = esp32-s3-devkitc-1 ;; generic dev board; the next line adds PSRAM support
-board_build.arduino.memory_type = qio_qspi ;; use with PSRAM: 2MB or 4MB
-
[env:esp8285_4CH_MagicHome]
board = esp8285
platform = ${esp8266.platform_wled_default}
platform_packages = ${esp8266.platform_packages}
board_build.ldscript = ${common.ldscript_1m128k}
build_unflags = ${common.build_unflags}
-build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_DISABLE_OTA
+build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_DISABLE_OTA -D WLED_RELEASE_NAME=\"ESP8285_4CH_MagicHome\" #-DWLED_DISABLE_2D
+ -D WLED_DISABLE_PARTICLESYSTEM2D
lib_deps = ${esp8266.lib_deps}
[env:esp8285_H801]
@@ -308,7 +303,8 @@ platform = ${esp8266.platform_wled_default}
platform_packages = ${esp8266.platform_packages}
board_build.ldscript = ${common.ldscript_1m128k}
build_unflags = ${common.build_unflags}
-build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_DISABLE_OTA
+build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_DISABLE_OTA -D WLED_RELEASE_NAME=\"ESP8285_H801\" #-DWLED_DISABLE_2D
+ -D WLED_DISABLE_PARTICLESYSTEM2D
lib_deps = ${esp8266.lib_deps}
[env:d1_mini_5CH_Shojo_PCB]
@@ -318,6 +314,8 @@ platform_packages = ${esp8266.platform_packages}
board_build.ldscript = ${common.ldscript_4m1m}
build_unflags = ${common.build_unflags}
build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_USE_SHOJO_PCB ;; NB: WLED_USE_SHOJO_PCB is not used anywhere in the source code. Not sure why its needed.
+ -D WLED_RELEASE_NAME=\"ESP8266_5CH_Shojo_PCB\" #-DWLED_DISABLE_2D
+ -D WLED_DISABLE_PARTICLESYSTEM2D
lib_deps = ${esp8266.lib_deps}
[env:d1_mini_debug]
@@ -327,7 +325,8 @@ platform = ${esp8266.platform_wled_default}
platform_packages = ${esp8266.platform_packages}
board_build.ldscript = ${common.ldscript_4m1m}
build_unflags = ${common.build_unflags}
-build_flags = ${common.build_flags} ${esp8266.build_flags} ${common.debug_flags}
+build_flags = ${common.build_flags} ${esp8266.build_flags} ${common.debug_flags} -D WLED_RELEASE_NAME=\"ESP8266_D1MINI_DEBUG\" #-DWLED_DISABLE_2D
+ -D WLED_DISABLE_PARTICLESYSTEM2D
lib_deps = ${esp8266.lib_deps}
[env:d1_mini_ota]
@@ -339,7 +338,8 @@ platform = ${esp8266.platform_wled_default}
platform_packages = ${esp8266.platform_packages}
board_build.ldscript = ${common.ldscript_4m1m}
build_unflags = ${common.build_unflags}
-build_flags = ${common.build_flags} ${esp8266.build_flags}
+build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP8266_D1MINI_OTA\" #-DWLED_DISABLE_2D
+ -D WLED_DISABLE_PARTICLESYSTEM2D
lib_deps = ${esp8266.lib_deps}
[env:anavi_miracle_controller]
@@ -349,6 +349,8 @@ platform_packages = ${esp8266.platform_packages}
board_build.ldscript = ${common.ldscript_4m1m}
build_unflags = ${common.build_unflags}
build_flags = ${common.build_flags} ${esp8266.build_flags} -D DATA_PINS=12 -D IRPIN=-1 -D RLYPIN=2
+ -D WLED_RELEASE_NAME=\"ESP8266_ANAVI_MIRACLE\" #-DWLED_DISABLE_2D
+ -D WLED_DISABLE_PARTICLESYSTEM2D
lib_deps = ${esp8266.lib_deps}
[env:esp32c3dev_2MB]
@@ -359,6 +361,7 @@ platform = ${esp32c3.platform}
platform_packages = ${esp32c3.platform_packages}
board = esp32-c3-devkitm-1
build_flags = ${common.build_flags} ${esp32c3.build_flags}
+ -D WLED_RELEASE_NAME=\"ESP32-C3_2MB\"
-D WLED_WATCHDOG_TIMEOUT=0
-D WLED_DISABLE_OTA
; -DARDUINO_USB_CDC_ON_BOOT=1 ;; for virtual CDC USB
@@ -410,12 +413,14 @@ board_build.f_flash = 80000000L
[env:m5atom]
extends = env:esp32dev # we want to extend the existing esp32dev environment (and define only updated options)
build_flags = ${common.build_flags} ${esp32.build_flags} -D DATA_PINS=27 -D BTNPIN=39
+ -D WLED_RELEASE_NAME=\"ESP32_m5atom\"
[env:sp501e]
board = esp_wroom_02
platform = ${esp8266.platform_wled_default}
board_build.ldscript = ${common.ldscript_2m512k}
build_flags = ${common.build_flags} ${esp8266.build_flags} -D DATA_PINS=3 -D BTNPIN=1
+ -D WLED_RELEASE_NAME=\"ESP8266_sp501e\" -D WLED_DISABLE_PARTICLESYSTEM2D
lib_deps = ${esp8266.lib_deps}
[env:sp511e]
@@ -423,6 +428,7 @@ board = esp_wroom_02
platform = ${esp8266.platform_wled_default}
board_build.ldscript = ${common.ldscript_2m512k}
build_flags = ${common.build_flags} ${esp8266.build_flags} -D DATA_PINS=3 -D BTNPIN=2 -D IRPIN=5 -D WLED_MAX_BUTTONS=3
+ -D WLED_RELEASE_NAME=\"ESP8266_sp511e\" -D WLED_DISABLE_PARTICLESYSTEM2D
lib_deps = ${esp8266.lib_deps}
[env:Athom_RGBCW] ;7w and 5w(GU10) bulbs
@@ -432,7 +438,8 @@ platform_packages = ${esp8266.platform_packages}
board_build.ldscript = ${common.ldscript_2m512k}
build_unflags = ${common.build_unflags}
build_flags = ${common.build_flags} ${esp8266.build_flags} -D BTNPIN=-1 -D RLYPIN=-1 -D DATA_PINS=4,12,14,13,5
- -D LED_TYPES=TYPE_ANALOG_5CH -D WLED_DISABLE_INFRARED -D WLED_MAX_CCT_BLEND=0
+ -D LED_TYPES=TYPE_ANALOG_5CH -D WLED_DISABLE_INFRARED -D WLED_MAX_CCT_BLEND=0
+ -D WLED_RELEASE_NAME=\"ESP8285_Athom_RGBCW\" -D WLED_DISABLE_PARTICLESYSTEM2D
lib_deps = ${esp8266.lib_deps}
[env:Athom_15w_RGBCW] ;15w bulb
@@ -442,7 +449,8 @@ platform_packages = ${esp8266.platform_packages}
board_build.ldscript = ${common.ldscript_2m512k}
build_unflags = ${common.build_unflags}
build_flags = ${common.build_flags} ${esp8266.build_flags} -D BTNPIN=-1 -D RLYPIN=-1 -D DATA_PINS=4,12,14,5,13
- -D LED_TYPES=TYPE_ANALOG_5CH -D WLED_DISABLE_INFRARED -D WLED_MAX_CCT_BLEND=0 -D WLED_USE_IC_CCT
+ -D LED_TYPES=TYPE_ANALOG_5CH -D WLED_DISABLE_INFRARED -D WLED_MAX_CCT_BLEND=0 -D WLED_USE_IC_CCT
+ -D WLED_RELEASE_NAME=\"ESP8285_Athom_15W_RGBCW\" -D WLED_DISABLE_PARTICLESYSTEM2D
lib_deps = ${esp8266.lib_deps}
[env:Athom_3Pin_Controller] ;small controller with only data
@@ -452,6 +460,7 @@ platform_packages = ${esp8266.platform_packages}
board_build.ldscript = ${common.ldscript_2m512k}
build_unflags = ${common.build_unflags}
build_flags = ${common.build_flags} ${esp8266.build_flags} -D BTNPIN=0 -D RLYPIN=-1 -D DATA_PINS=1 -D WLED_DISABLE_INFRARED
+ -D WLED_RELEASE_NAME=\"ESP8285_Athom_3Pin\" -D WLED_DISABLE_PARTICLESYSTEM2D
lib_deps = ${esp8266.lib_deps}
[env:Athom_4Pin_Controller] ; With clock and data interface
@@ -461,6 +470,7 @@ platform_packages = ${esp8266.platform_packages}
board_build.ldscript = ${common.ldscript_2m512k}
build_unflags = ${common.build_unflags}
build_flags = ${common.build_flags} ${esp8266.build_flags} -D BTNPIN=0 -D RLYPIN=12 -D DATA_PINS=1 -D WLED_DISABLE_INFRARED
+ -D WLED_RELEASE_NAME=\"ESP8285_Athom_4Pin\" -D WLED_DISABLE_PARTICLESYSTEM2D
lib_deps = ${esp8266.lib_deps}
[env:Athom_5Pin_Controller] ;Analog light strip controller
@@ -470,6 +480,7 @@ platform_packages = ${esp8266.platform_packages}
board_build.ldscript = ${common.ldscript_2m512k}
build_unflags = ${common.build_unflags}
build_flags = ${common.build_flags} ${esp8266.build_flags} -D BTNPIN=0 -D RLYPIN=-1 DATA_PINS=4,12,14,13 -D WLED_DISABLE_INFRARED
+ -D WLED_RELEASE_NAME=\"ESP8285_Athom_5Pin\" -D WLED_DISABLE_PARTICLESYSTEM2D
lib_deps = ${esp8266.lib_deps}
[env:MY9291]
@@ -478,7 +489,8 @@ platform = ${esp8266.platform_wled_default}
platform_packages = ${esp8266.platform_packages}
board_build.ldscript = ${common.ldscript_1m128k}
build_unflags = ${common.build_unflags}
-build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_DISABLE_OTA -D USERMOD_MY9291
+custom_usermods = ${env:esp01_1m_full.custom_usermods} MY9291
+build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_DISABLE_OTA -D WLED_RELEASE_NAME=\"ESP8266_MY9291\" -D WLED_DISABLE_PARTICLESYSTEM2D
lib_deps = ${esp8266.lib_deps}
# ------------------------------------------------------------------------------
@@ -492,7 +504,7 @@ platform = ${esp8266.platform_wled_default}
platform_packages = ${esp8266.platform_packages}
board_build.ldscript = ${common.ldscript_2m512k}
build_unflags = ${common.build_unflags}
-build_flags = ${common.build_flags} ${esp8266.build_flags}
+build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP8266_CODM06_2MB\" -D WLED_DISABLE_PARTICLESYSTEM2D
lib_deps = ${esp8266.lib_deps}
[env:codm-controller-0_6-rev2]
@@ -501,7 +513,7 @@ platform = ${esp8266.platform_wled_default}
platform_packages = ${esp8266.platform_packages}
board_build.ldscript = ${common.ldscript_4m1m}
build_unflags = ${common.build_unflags}
-build_flags = ${common.build_flags} ${esp8266.build_flags}
+build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP8266_CODM06R2_4MB\" -D WLED_DISABLE_PARTICLESYSTEM2D
lib_deps = ${esp8266.lib_deps}
# ------------------------------------------------------------------------------
diff --git a/readme.md b/readme.md
index bcdf5ab303..762be9bd43 100644
--- a/readme.md
+++ b/readme.md
@@ -7,81 +7,97 @@
-
-
+
# Welcome to WLED! ✨
-A fast and feature-rich implementation of an ESP32 and ESP8266 webserver to control NeoPixel (WS2812B, WS2811, SK6812) LEDs or also SPI based chipsets like the WS2801 and APA102!
+A fast and feature-rich firmware for ESP32 microcontrollers to control addressable LEDs — from simple strips to large 2D matrices and HUB75 panels.
-Originally created by [Aircoookie](https://github.com/Aircoookie)
+Originally created by [Aircoookie](https://github.com/Aircoookie), now maintained by a community of contributors.
## ⚙️ Features
-- WS2812FX library with more than 100 special effects
-- FastLED noise effects and 50 palettes
-- Modern UI with color, effect and segment controls
-- Segments to set different effects and colors to user defined parts of the LED string
-- Settings page - configuration via the network
-- Access Point and station mode - automatic failsafe AP
-- [Up to 10 LED outputs](https://kno.wled.ge/features/multi-strip/#esp32) per instance
-- Support for RGBW strips
-- Up to 250 user presets to save and load colors/effects easily, supports cycling through them.
-- Presets can be used to automatically execute API calls
-- Nightlight function (gradually dims down)
-- Full OTA software updateability (HTTP + ArduinoOTA), password protectable
-- Configurable analog clock (Cronixie, 7-segment and EleksTube IPS clock support via usermods)
-- Configurable Auto Brightness limit for safe operation
-- Filesystem-based config for easier backup of presets and settings
-
-## 💡 Supported light control interfaces
-- WLED app for [Android](https://play.google.com/store/apps/details?id=ca.cgagnier.wlednativeandroid) and [iOS](https://apps.apple.com/gb/app/wled-native/id6446207239)
-- JSON and HTTP request APIs
-- MQTT
-- E1.31, Art-Net, DDP and TPM2.net
-- [diyHue](https://github.com/diyhue/diyHue) (Wled is supported by diyHue, including Hue Sync Entertainment under udp. Thanks to [Gregory Mallios](https://github.com/gmallios))
-- [Hyperion](https://github.com/hyperion-project/hyperion.ng)
-- UDP realtime
-- Alexa voice control (including dimming and color)
-- Sync to Philips hue lights
-- Adalight (PC ambilight via serial) and TPM2
-- Sync color of multiple WLED devices (UDP notifier)
-- Infrared remotes (24-key RGB, receiver required)
-- Simple timers/schedules (time from NTP, timezones/DST supported)
+
+### Effects & Visuals
+- [**200+ built-in effects**](https://kno.wled.ge/features/effects/) including classic animations, audio-reactive, and 2D/matrix effects
+- [50+ color palettes](https://kno.wled.ge/features/palettes/) plus a built-in **custom palette editor** (PixelForge)
+- [**2D LED matrix support**](https://kno.wled.ge/advanced/mapping/) with dedicated 2D effects and flexible panel mapping
+- [**HUB75 RGB matrix panel support**](https://kno.wled.ge/advanced/HUB75/) (ESP32)
+- [**AudioReactive**](https://kno.wled.ge/advanced/audio-reactive/) effects — included by default, responding to sound via microphone, line-in, or network audio source
+- Effect blending for smooth transitions between animations
+- Antialiased drawing functions for smooth graphics
+
+### Segments & Control
+- [**Segments**](https://kno.wled.ge/features/segments/) — apply different effects, colors and palettes to independent parts of your LED setup simultaneously
+- Up to **250 presets** to save and recall colors, effects and segment configurations — supports [playlists](https://kno.wled.ge/features/presets/) for automated cycling
+- Nightlight function with configurable dimming curve
+- Configurable **Auto Brightness Limiter** (per output) for safe operation
+
+### Hardware Support
+- **ESP32** (all variants: original, S2, S3, C3)
+- [**Up to 17 LED outputs**](https://kno.wled.ge/features/multi-strip/) on ESP32 using parallel I2S + RMT
+- [Addressable LED support](https://kno.wled.ge/basics/compatible-led-strips/): WS2812B, WS2811, WS2815, SK6812, WS2805, TM1914, APA102, WS2801, LPD8806, and many more
+- RGBW, [RGB+CCT](https://kno.wled.ge/features/cct/) and white-only strips
+- PWM outputs for analog LEDs and dimmers
+- [**Ethernet** support](https://kno.wled.ge/features/ethernet-lan/) for a wide range of boards (QuinLED, LILYGO, Olimex, and more)
+- Filesystem-based config for easy backup and restore of presets and settings
+- Full OTA firmware updates (HTTP + ArduinoOTA), password-protectable
+
+### Connectivity & Integrations
+- **WLED app** for [Android](https://play.google.com/store/apps/details?id=ca.cgagnier.wlednativeandroid) and [iOS](https://apps.apple.com/gb/app/wled-native/id6446207239)
+- [JSON](https://kno.wled.ge/interfaces/json-api/) and [HTTP request](https://kno.wled.ge/interfaces/http-api/) APIs
+- **Multi-WiFi** — connect to up to 3 networks with automatic AP fallback
+- **ESP-NOW** wireless sync between devices (no WiFi router required)
+- [**MQTT**](https://kno.wled.ge/interfaces/mqtt/) with Home Assistant discovery
+- [**E1.31, Art-Net**](https://kno.wled.ge/interfaces/e1.31-dmx/), [DDP](https://kno.wled.ge/interfaces/ddp/) and [TPM2.net](https://kno.wled.ge/interfaces/udp-realtime/) for DMX/professional lighting control
+- [UDP realtime sync](https://kno.wled.ge/interfaces/udp-notifier/) across multiple WLED devices
+- Alexa voice control (on/off, brightness, color)
+- [Philips Hue sync](https://kno.wled.ge/interfaces/philips-hue/)
+- [diyHue](https://github.com/diyhue/diyHue) and [Hyperion](https://github.com/hyperion-project/hyperion.ng) integration
+- [Adalight / TPM2](https://kno.wled.ge/interfaces/serial/) (PC ambilight via serial)
+- [Infrared remote control](https://kno.wled.ge/interfaces/infrared/) (24-key RGB, receiver required)
+- Timers and schedules (NTP time sync, full timezone and DST support)
+
+### Developer-Friendly
+- **Usermod system** — extend WLED with community or custom modules without modifying core code
+- Large and active [usermod library](https://kno.wled.ge/advanced/community-usermods/) including AudioReactive, temperature sensors, rotary encoders, displays, and much more
+- Well-documented [JSON API](https://kno.wled.ge/interfaces/json-api/)
+- Licensed under the **EUPL v1.2**
## 📲 Quick start guide and documentation
-See the [documentation on our official site](https://kno.wled.ge)!
+See the [documentation at kno.wled.ge](https://kno.wled.ge)!
-[On this page](https://kno.wled.ge/basics/tutorials/) you can find excellent tutorials and tools to help you get your new project up and running!
+[Tutorials and getting-started guides](https://kno.wled.ge/basics/tutorials/) to help you get your project running quickly.
## 🖼️ User interface
+
## 💾 Compatible hardware
-See [here](https://kno.wled.ge/basics/compatible-hardware)!
+See the [compatible hardware list](https://kno.wled.ge/basics/compatible-hardware) on the wiki.
## ✌️ Other
-Licensed under the EUPL v1.2 license
-Credits [here](https://kno.wled.ge/about/contributors/)!
-CORS proxy by [Corsfix](https://corsfix.com/)
+Licensed under the [EUPL v1.2](https://raw.githubusercontent.com/wled-dev/WLED/main/LICENSE).
+Credits to all [contributors](https://kno.wled.ge/about/contributors/)!
+CORS proxy by [Corsfix](https://corsfix.com/).
Join the Discord server to discuss everything about WLED!
-Check out the WLED [Discourse forum](https://wled.discourse.group)!
-
-You can also send me mails to [dev.aircoookie@gmail.com](mailto:dev.aircoookie@gmail.com), but please, only do so if you want to talk to me privately.
+Check out the WLED [Discourse forum](https://wled.discourse.group)!
-If WLED really brightens up your day, you can [](https://paypal.me/aircoookie)
+If you'd like to reach the original creator privately: [dev.aircoookie@gmail.com](mailto:dev.aircoookie@gmail.com).
+If WLED brightens up your day, you can [send a gift to Aircoookie via PayPal](https://paypal.me/aircoookie).
-*Disclaimer:*
+---
-If you are prone to photosensitive epilepsy, we recommended you do **not** use this software.
-If you still want to try, don't use strobe, lighting or noise modes or high effect speed settings.
+*Disclaimer:*
-As per the EUPL license, I assume no liability for any damage to you or any other person or equipment.
+If you are prone to photosensitive epilepsy, we recommend you do **not** use this software.
+If you still want to try, avoid strobe, lightning or noise modes and high effect speed settings.
+As per the EUPL license, no liability is assumed for any damage to you or any other person or equipment.
diff --git a/usermods/DHT/DHT.cpp b/usermods/DHT/DHT.cpp
index 2ed3dd0ace..ca210abadc 100644
--- a/usermods/DHT/DHT.cpp
+++ b/usermods/DHT/DHT.cpp
@@ -1,5 +1,5 @@
#include "wled.h"
-#ifdef WLED_DISABLE_MQTT
+#if defined(USERMOD_DHT_MQTT) && defined(WLED_DISABLE_MQTT)
#error "This user mod requires MQTT to be enabled."
#endif
@@ -246,4 +246,4 @@ class UsermodDHT : public Usermod {
static UsermodDHT dht;
-REGISTER_USERMOD(dht);
\ No newline at end of file
+REGISTER_USERMOD(dht);
diff --git a/usermods/EXAMPLE/usermod_v2_example.cpp b/usermods/EXAMPLE/usermod_v2_example.cpp
index 02e399fe08..65f3eda457 100644
--- a/usermods/EXAMPLE/usermod_v2_example.cpp
+++ b/usermods/EXAMPLE/usermod_v2_example.cpp
@@ -114,7 +114,7 @@ class MyExampleUsermod : public Usermod {
void loop() override {
// if usermod is disabled or called during strip updating just exit
// NOTE: on very long strips strip.isUpdating() may always return true so update accordingly
- if (!enabled || strip.isUpdating()) return;
+ if (!enabled || (strip.isUpdating() && (millis() - lastTime < 200))) return; // adjust "200" (in millisecond) to your needs - prevents starvation with very long strips
// do your magic here
if (millis() - lastTime > 1000) {
@@ -176,7 +176,7 @@ class MyExampleUsermod : public Usermod {
JsonObject usermod = root[FPSTR(_name)];
if (!usermod.isNull()) {
// expect JSON usermod data in usermod name object: {"ExampleUsermod:{"user0":10}"}
- userVar0 = usermod["user0"] | userVar0; //if "user0" key exists in JSON, update, else keep old value
+ userVar0 = usermod["user0"] | userVar0; //if "user0" key exists in JSON, update, else keep old value (userVar0 is defined in wled.h)
}
// you can as well check WLED state JSON keys
//if (root["bri"] == 255) Serial.println(F("Don't burn down your garage!"));
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 5ffc7968aa..196fba353c 100644
--- a/usermods/audioreactive/audio_reactive.cpp
+++ b/usermods/audioreactive/audio_reactive.cpp
@@ -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
@@ -1300,7 +1303,12 @@ class AudioReactive : public Usermod {
size_t packetSize = fftUdp.parsePacket();
#ifdef ARDUINO_ARCH_ESP32
- if ((packetSize > 0) && ((packetSize < 5) || (packetSize > UDPSOUND_MAX_PACKET))) fftUdp.flush(); // discard invalid packets (too small or too big) - only works on esp32
+ if ((packetSize > 0) && ((packetSize < 5) || (packetSize > UDPSOUND_MAX_PACKET)))
+ #if ESP_IDF_VERSION_MAJOR < 5
+ fftUdp.flush(); // discard invalid packets (too small or too big) - only works on esp32
+ #else
+ fftUdp.clear(); // function was renamed in newer frameworks
+ #endif
#endif
if ((packetSize > 5) && (packetSize <= UDPSOUND_MAX_PACKET)) {
//DEBUGSR_PRINTLN("Received UDP Sync Packet");
@@ -1533,14 +1541,9 @@ class AudioReactive : public Usermod {
// We cannot wait indefinitely before processing audio data
if (strip.isUpdating() && (millis() - lastUMRun < 2)) return; // be nice, but not too nice
- // suspend local sound processing when "real time mode" is active (E131, UDP, ADALIGHT, ARTNET)
- if ( (realtimeOverride == REALTIME_OVERRIDE_NONE) // please add other overrides here if needed
- &&( (realtimeMode == REALTIME_MODE_GENERIC)
- ||(realtimeMode == REALTIME_MODE_E131)
- ||(realtimeMode == REALTIME_MODE_UDP)
- ||(realtimeMode == REALTIME_MODE_ADALIGHT)
- ||(realtimeMode == REALTIME_MODE_ARTNET) ) ) // please add other modes here if needed
- {
+ // suspend local sound processing when "real time mode" is active (E131, UDP, ADALIGHT, ARTNET, DDP, DMX)
+ // exception: sound input is still needed when useMainSegmentOnly - other segments are still running with local input.
+ if (realtimeMode && !realtimeOverride && !useMainSegmentOnly) {
#if defined(ARDUINO_ARCH_ESP32) && defined(WLED_DEBUG)
if ((disableSoundProcessing == false) && (audioSyncEnabled == 0)) { // we just switched to "disabled"
DEBUG_PRINTLN(F("[AR userLoop] realtime mode active - audio processing suspended."));
@@ -1617,7 +1620,11 @@ class AudioReactive : public Usermod {
have_new_sample = receiveAudioData();
if (have_new_sample) last_UDPTime = millis();
#ifdef ARDUINO_ARCH_ESP32
+ #if ESP_IDF_VERSION_MAJOR < 5
else fftUdp.flush(); // Flush udp input buffers if we haven't read it - avoids hickups in receive mode. Does not work on 8266.
+ #else
+ else fftUdp.clear(); // function was renamed in newer frameworks
+ #endif
#endif
lastTime = millis();
}
@@ -1722,7 +1729,7 @@ class AudioReactive : public Usermod {
);
}
micDataReal = 0.0f; // just to be sure
- if (enabled) disableSoundProcessing = false;
+ if (enabled) disableSoundProcessing = false; // allows FFT_Task to run at least once, even when loop() might disable again
updateIsRunning = init;
}
@@ -2067,6 +2074,13 @@ class AudioReactive : public Usermod {
bool configComplete = !top.isNull();
bool oldEnabled = enabled;
bool oldAddPalettes = addPalettes;
+ #ifdef ARDUINO_ARCH_ESP32
+ auto oldDMType = dmType;
+ auto oldI2SsdPin = i2ssdPin;
+ auto oldI2swsPin = i2swsPin;
+ auto oldI2SckPin = i2sckPin;
+ auto oldI2SmclkPin = mclkPin;
+ #endif
configComplete &= getJsonValue(top[FPSTR(_enabled)], enabled);
configComplete &= getJsonValue(top[FPSTR(_addPalettes)], addPalettes);
@@ -2108,6 +2122,15 @@ class AudioReactive : public Usermod {
// add/remove custom/audioreactive palettes
if ((oldAddPalettes && !addPalettes) || (oldAddPalettes && !enabled)) removeAudioPalettes();
if ((addPalettes && !oldAddPalettes && enabled) || (addPalettes && !oldEnabled && enabled)) createAudioPalettes();
+ #ifdef ARDUINO_ARCH_ESP32
+ // notify user when a reboot is necessary
+ if ((audioSource != nullptr) && (oldDMType != dmType)) errorFlag = ERR_REBOOT_NEEDED; // changing mic type requires reboot
+ if ( (audioSource != nullptr) && (enabled==true)
+ && ((oldI2SsdPin != i2ssdPin) || (oldI2swsPin != i2swsPin) || (oldI2SckPin != i2sckPin)) ) errorFlag = ERR_REBOOT_NEEDED; // changing mic pins requires reboot
+ if ((audioSource != nullptr) && (oldI2SmclkPin != mclkPin)) errorFlag = ERR_REBOOT_NEEDED; // changing MCLK pin requires reboot
+ if ((oldDMType != dmType) && (oldDMType == 0)) errorFlag = ERR_POWEROFF_NEEDED; // changing from analog mic requires power cycle
+ if ((oldDMType != dmType) && (dmType == 0)) errorFlag = ERR_POWEROFF_NEEDED; // changing to analog mic requires power cycle
+ #endif
} // else setup() will create palettes
return configComplete;
}
diff --git a/usermods/platformio_override.usermods.ini b/usermods/platformio_override.usermods.ini
index 6a402c2f73..fbcb9d3d0c 100644
--- a/usermods/platformio_override.usermods.ini
+++ b/usermods/platformio_override.usermods.ini
@@ -3,12 +3,16 @@ default_envs = usermods_esp32, usermods_esp32c3, usermods_esp32s2, usermods_esp3
[env:usermods_esp32]
extends = env:esp32dev
+build_flags = ${env:esp32dev.build_flags}
+ -DTOUCH_CS=9 -D WLED_USE_SD_SPI ;; help a few usermods that require special flags to compile
custom_usermods = ${usermods.custom_usermods}
board_build.partitions = ${esp32.extreme_partitions} ; We're gonna need a bigger boat
[env:usermods_esp32c3]
extends = env:esp32c3dev
+build_flags = ${env:esp32c3dev.build_flags}
+ -DTOUCH_CS=9 -D WLED_USE_SD_SPI ;; help a few usermods that require special flags to compile
board = esp32-c3-devkitm-1
custom_usermods = ${usermods.custom_usermods}
board_build.partitions = ${esp32.extreme_partitions} ; We're gonna need a bigger boat
@@ -16,12 +20,17 @@ board_build.partitions = ${esp32.extreme_partitions} ; We're gonna need a bigge
[env:usermods_esp32s2]
extends = env:lolin_s2_mini
+build_flags = ${env:lolin_s2_mini.build_flags}
+ -DTOUCH_CS=9 -D WLED_USE_SD_SPI ;; help a few usermods that require special flags to compile
custom_usermods = ${usermods.custom_usermods}
board_build.partitions = ${esp32.extreme_partitions} ; We're gonna need a bigger boat
[env:usermods_esp32s3]
extends = env:esp32s3dev_16MB_opi
+build_flags = ${env:esp32s3dev_16MB_opi.build_flags}
+ -DTOUCH_CS=9 -D WLED_USE_SD_SPI ;; help a few usermods that require special flags to compile
+ -D WLED_DEBUG ;; try to catch broken DEBUG_PRINT statements
custom_usermods = ${usermods.custom_usermods}
board_build.partitions = ${esp32.extreme_partitions} ; We're gonna need a bigger boat
diff --git a/usermods/user_fx/user_fx.cpp b/usermods/user_fx/user_fx.cpp
index cf7a651db6..bc71bbba69 100644
--- a/usermods/user_fx/user_fx.cpp
+++ b/usermods/user_fx/user_fx.cpp
@@ -2,6 +2,28 @@
// for information how FX metadata strings work see https://kno.wled.ge/interfaces/json-api/#effect-metadata
+#if !(defined(WLED_DISABLE_PARTICLESYSTEM2D) && defined(WLED_DISABLE_PARTICLESYSTEM1D))
+ #include "FXparticleSystem.h" // include particle system code only if at least one system is enabled
+ #ifdef WLED_DISABLE_PARTICLESYSTEM2D
+ #define WLED_PS_DONT_REPLACE_2D_FX
+ #endif
+ #ifdef WLED_DISABLE_PARTICLESYSTEM1D
+ #define WLED_PS_DONT_REPLACE_1D_FX
+ #endif
+ #ifdef ESP8266
+ #if !defined(WLED_DISABLE_PARTICLESYSTEM2D) && !defined(WLED_DISABLE_PARTICLESYSTEM1D)
+ #error ESP8266 does not support 1D and 2D particle systems simultaneously. Please disable one of them.
+ #endif
+ #endif
+#else
+ #define WLED_PS_DONT_REPLACE_1D_FX
+ #define WLED_PS_DONT_REPLACE_2D_FX
+#endif
+#ifdef WLED_PS_DONT_REPLACE_FX
+ #define WLED_PS_DONT_REPLACE_1D_FX
+ #define WLED_PS_DONT_REPLACE_2D_FX
+#endif
+
// paletteBlend: 0 - wrap when moving, 1 - always wrap, 2 - never wrap, 3 - none (undefined)
#define PALETTE_SOLID_WRAP (paletteBlend == 1 || paletteBlend == 3)
@@ -1258,6 +1280,156 @@ static void mode_morsecode(void) {
static const char _data_FX_MODE_MORSECODE[] PROGMEM = "Morse Code@Speed,,,,Color mode,Color by Word,Punctuation,EndOfMessage;;!;1;sx=192,c3=8,o1=1,o2=1";
+/*
+/ PS Pendulum effect (uses the Particle System)
+* by Bob Loeffler and claude.ai Particle System by dedehai
+* First slider (speed) is for the speed of the pendulum LED going back and forth. (0 = random speed)
+* Second slider (intensity) is for the X offset. In the middle means the center of the pendulum will be in the middle of the LED strip.
+* Moving the slider left will move the pendulum to the left, etc. (0 = random X offset)
+* Third slider (blur) is for how much of a trail the pendulum will leave.
+* Fourth slider (pause/delay) is for how long the effect will pause before restarting the pendulum movement.
+* Fifth slider (gravity/damping) is for how much the pendulum movement will be dampened due to gravity or friction. (0 = random damping)
+* Checkbox1 will select the LED color based on it's position on the strip.
+* aux0 stores the settings checksum to detect changes.
+*/
+#define PENDULUM_MIN_AMPLITUDE_SUBPX ((PS_P_RADIUS_1D * 2) / 5)
+
+// Custom pendulum state, stored in the extra bytes after the particle system's own data
+typedef struct {
+ int32_t currentPos; // sub-pixel position (scaled by PS_P_RADIUS_1D) — matches particle.x units directly
+ int32_t fromPos;
+ int32_t toPos;
+ int32_t amplitude; // sub-pixel
+ uint16_t damping256; // Q8 fixed point: 0-256 represents 0.0-1.0
+ uint32_t swingStart;
+ uint32_t swingDuration;
+ uint32_t pauseStart;
+ uint16_t swing_time;
+ uint16_t offsetX; // pixel units (kept as pixels since map() needs it that way)
+ uint8_t phase;
+ uint8_t goingLeft;
+} PendulumData;
+
+void mode_particlePendulum(void) {
+ ParticleSystem1D *PartSys = nullptr;
+ PendulumData *pd = nullptr;
+
+ if (SEGMENT.call == 0) {
+ if (!initParticleSystem1D(PartSys, 0, 1, sizeof(PendulumData)))
+ FX_FALLBACK_STATIC;
+ } else {
+ PartSys = reinterpret_cast(SEGENV.data);
+ }
+ if (PartSys == nullptr) FX_FALLBACK_STATIC;
+
+ PartSys->updateSystem();
+ pd = reinterpret_cast(PartSys->PSdataEnd);
+
+ uint16_t segLen = SEGLEN;
+ uint16_t segCenter = segLen / 2;
+ uint16_t pause_delay = map(SEGMENT.custom2, 0, 255, 500, 15000);
+ uint16_t cutoff = segLen / 10;
+
+ constexpr uint8_t PHASE_STOPPED = 0U;
+ constexpr uint8_t PHASE_SWING = 1U;
+ constexpr uint8_t PHASE_PAUSED = 2U;
+
+ PartSys->setUsedParticles(0);
+ PartSys->setWrap(false);
+ PartSys->setBounce(false);
+ PartSys->setMotionBlur(SEGMENT.custom1); // amount of blurring
+ PartSys->setColorByPosition(SEGMENT.check1);
+
+ uint32_t settingssum = SEGMENT.speed + SEGMENT.intensity + SEGMENT.custom1 + SEGMENT.custom2 + SEGMENT.custom3;
+ bool settingsChanged = (SEGENV.aux0 != settingssum);
+
+ if (SEGENV.call == 0 || pd->phase == PHASE_STOPPED || settingsChanged) {
+ const uint8_t speed = SEGMENT.speed;
+ if (speed == 0) pd->swing_time = hw_random16(1000, 2400);
+ else pd->swing_time = map(speed, 1, 255, 2400, 400);
+
+ const uint8_t intensity = SEGMENT.intensity;
+ if (intensity == 0) pd->offsetX = hw_random16(cutoff, segLen - cutoff);
+ else pd->offsetX = map(intensity, 1, 255, cutoff, segLen - cutoff);
+
+ // Damping as Q8 fixed-point (0-256 = 0.0-1.0)
+ const uint8_t custom3 = SEGMENT.custom3;
+ uint8_t dampingPct;
+ if (custom3 == 0) dampingPct = hw_random8(75, 95);
+ else dampingPct = map(custom3, 1, 31, 95, 75);
+ pd->damping256 = ((uint16_t)dampingPct * 256) / 100;
+
+ // Amplitude in pixels first, then converted to sub-pixel units
+ uint16_t amplitude_px;
+ if (pd->offsetX < segCenter) amplitude_px = pd->offsetX;
+ else amplitude_px = MIN(pd->offsetX, (uint16_t)(segLen - 1 - pd->offsetX));
+ pd->amplitude = (int32_t)amplitude_px * PS_P_RADIUS_1D;
+
+ pd->fromPos = (int32_t)pd->offsetX * PS_P_RADIUS_1D;
+ pd->toPos = pd->fromPos - pd->amplitude;
+ pd->swingStart = strip.now;
+ pd->swingDuration = pd->swing_time;
+ pd->phase = PHASE_SWING;
+ pd->goingLeft = 1;
+ pd->currentPos = pd->fromPos;
+
+ PartSys->particles[0].ttl = 300;
+ PartSys->particleFlags[0].perpetual = true;
+ PartSys->particleFlags[0].fixed = true;
+
+ SEGENV.aux0 = settingssum;
+ }
+
+ if (pd->phase == PHASE_SWING) {
+ uint32_t elapsed = strip.now - pd->swingStart;
+ if (elapsed > pd->swingDuration) elapsed = pd->swingDuration;
+
+ // angle: 0 -> 32768 represents 0 -> 180 degrees in cos16_t's 16-bit angle space
+ uint32_t angle = pd->swingDuration ? ((uint32_t)elapsed * 32768) / pd->swingDuration : 32768;
+
+ int32_t cosT16 = cos16_t((uint16_t)angle); // inferred range: -32767..32767
+ int32_t frac16 = (32767 - cosT16) >> 1; // 0..32767, represents ease fraction * 32767
+
+ int32_t delta = pd->toPos - pd->fromPos; // sub-pixel
+ pd->currentPos = pd->fromPos + (int32_t)(((int64_t)delta * frac16) / 32767);
+
+ if (elapsed >= pd->swingDuration) {
+ pd->amplitude = (pd->amplitude * (int32_t)pd->damping256) >> 8; // (Q8 fixed-point)
+
+ if (pd->amplitude < PENDULUM_MIN_AMPLITUDE_SUBPX) {
+ pd->currentPos = (int32_t)pd->offsetX * PS_P_RADIUS_1D;
+ pd->phase = PHASE_PAUSED;
+ pd->pauseStart = strip.now;
+ } else {
+ int32_t offsetSubpx = (int32_t)pd->offsetX * PS_P_RADIUS_1D;
+ int32_t nextTo = offsetSubpx + (pd->goingLeft ? pd->amplitude : -pd->amplitude);
+ pd->fromPos = pd->toPos;
+ pd->toPos = nextTo;
+ pd->goingLeft = !(pd->goingLeft);
+ pd->swingStart = strip.now;
+
+ // duration scales with amplitude/half-segment-length, in Q8 fixed-point
+ int32_t halfSegSubpx = ((int32_t)(segLen - 1) * PS_P_RADIUS_1D) / 2;
+ int32_t ratio256 = halfSegSubpx ? (pd->amplitude * 256) / halfSegSubpx : 256;
+ uint32_t dur = ((uint32_t)pd->swing_time * ratio256) >> 8;
+ pd->swingDuration = (dur < 200) ? 200 : dur;
+ }
+ }
+ } else if (pd->phase == PHASE_PAUSED) {
+ pd->currentPos = (int32_t)pd->offsetX * PS_P_RADIUS_1D;
+ if (strip.now - pd->pauseStart >= pause_delay) {
+ pd->phase = PHASE_STOPPED;
+ }
+ }
+
+ PartSys->particles[0].x = pd->currentPos;
+ PartSys->particles[0].vx = 0;
+
+ PartSys->update();
+}
+static const char _data_FX_MODE_PS_PENDULUM[] PROGMEM = "PS Pendulum@Speed (0=random),X Offset (0=random),Blur,Pause/Delay,Damping/Gravity (0=random), Position Color;!,!;!;1;c2=32,c3=4,o1=1";
+
+
/////////////////////
// UserMod Class //
/////////////////////
@@ -1272,7 +1444,8 @@ class UserFxUsermod : public Usermod {
strip.addEffect(255, &mode_2D_magma, _data_FX_MODE_2D_MAGMA);
strip.addEffect(255, &mode_ants, _data_FX_MODE_ANTS);
strip.addEffect(255, &mode_morsecode, _data_FX_MODE_MORSECODE);
-
+ strip.addEffect(255, &mode_particlePendulum, _data_FX_MODE_PS_PENDULUM);
+
////////////////////////////////////////
// add your effect function(s) here //
////////////////////////////////////////
diff --git a/wled00/FX.cpp b/wled00/FX.cpp
index bd42d1a9eb..81bf6e11af 100644
--- a/wled00/FX.cpp
+++ b/wled00/FX.cpp
@@ -1061,7 +1061,7 @@ void mode_traffic_light(void) {
case 0: SEGMENT.setPixelColor(i, 0x00FF0000); mdelay = 150 + (100 * (uint32_t)(255 - SEGMENT.speed));break;
case 1: SEGMENT.setPixelColor(i, 0x00FF0000); mdelay = 150 + (20 * (uint32_t)(255 - SEGMENT.speed)); SEGMENT.setPixelColor(i+1, 0x00EECC00); break;
case 2: SEGMENT.setPixelColor(i+2, 0x0000FF00); mdelay = 150 + (100 * (uint32_t)(255 - SEGMENT.speed));break;
- case 3: SEGMENT.setPixelColor(i+1, 0x00EECC00); mdelay = 150 + (20 * (uint32_t)(255 - SEGMENT.speed));break;
+ case 3: SEGMENT.setPixelColor(i+1, gamma32inv(0x00EECC00)); mdelay = 150 + (20 * (uint32_t)(255 - SEGMENT.speed));break; // gamma inversion to restore original pre 16.0 looks
}
}
@@ -2345,7 +2345,7 @@ void mode_colortwinkle() {
unsigned index = i >> 3;
unsigned bitNum = i & 0x07;
bitWrite(SEGENV.data[index], bitNum, true);
- SEGMENT.setPixelColor(i, ColorFromPalette(SEGPALETTE, hw_random8(), 64, NOBLEND));
+ SEGMENT.setPixelColor(i, ColorFromPalette(SEGPALETTE, hw_random8(), gamma8inv(64), NOBLEND)); // note on gamma8inv: inverting results in non-linear brightness fade as originally designed
break; //only spawn 1 new pixel per frame per 50 LEDs
}
}
@@ -2616,7 +2616,7 @@ static CRGBW twinklefox_one_twinkle(uint32_t ms, uint8_t salt, bool cat)
unsigned hue = slowcycle8 - salt;
CRGBW c;
if (bright > 0) {
- c = ColorFromPalette(SEGPALETTE, hue, bright, NOBLEND);
+ c = ColorFromPalette(SEGPALETTE, hue, gamma8inv(bright), NOBLEND); // note on gamma8inv: inverting results in non-linear brightness fade as originally designed
if (!SEGMENT.check1) {
// This code takes a pixel, and if its in the 'fading down'
// part of the cycle, it adjusts the color a little bit like the
@@ -2651,19 +2651,18 @@ static void twinklefox_base(bool cat)
if (SEGMENT.speed > 100) SEGENV.aux0 = 3 + ((255 - SEGMENT.speed) >> 3);
else SEGENV.aux0 = 22 + ((100 - SEGMENT.speed) >> 1);
- // Set up the background color, "bg".
+ // Set up the background color, "bg". Note: using gamma invert for brightness as the FX was written without any gamma correction, it will dim down too much now
CRGBW bg = SEGCOLOR(1);
- unsigned bglight = bg.getAverageLight();
+ unsigned bglight = bg.getRGBaverage();
if (bglight > 64) {
- bg = color_fade(bg, 16, true); // very bright, so scale to 1/16th
+ bg = color_fade(bg, gamma8inv(16), true); // very bright, so scale to 1/16th
} else if (bglight > 16) {
- bg = color_fade(bg, 64, true); // not that bright, so scale to 1/4th
+ bg = color_fade(bg, gamma8inv(64), true); // not that bright, so scale to 1/4
} else {
- bg = color_fade(bg, 86, true); // dim, scale to 1/3rd.
+ bg = color_fade(bg, gamma8inv(86), true); // dim, scale to 1/3rd
}
- bg = gamma32inv(bg); // need to invert gamma as the FX was written without any gamma correction and it will dim down too much otherwise
- unsigned backgroundBrightness = bg.getAverageLight();
+ bglight = bg.getRGBaverage(); // update after scaling
for (unsigned i = 0; i < SEGLEN; i++) {
@@ -2680,8 +2679,8 @@ static void twinklefox_base(bool cat)
// on the "brightness = f( time )" idea.
CRGBW c = twinklefox_one_twinkle(myclock30, myunique8, cat);
- unsigned cbright = c.getAverageLight();
- int deltabright = cbright - backgroundBrightness;
+ unsigned cbright = c.getRGBaverage();
+ int deltabright = cbright - bglight;
if (deltabright >= 32 || (bg==0)) {
// If the new pixel is significantly brighter than the background color,
// use the new color.
@@ -3151,8 +3150,8 @@ static const char _data_FX_MODE_ROLLINGBALLS[] PROGMEM = "Rolling Balls@!,# of b
* aux1 is the main counter for timing.
*/
typedef struct PacManChars {
- signed pos;
- signed topPos; // LED position of farthest PacMan has moved
+ int pos;
+ int topPos; // LED position of farthest PacMan has moved
uint32_t color;
bool direction; // true = moving away from first LED
bool blue; // used for ghosts only
@@ -3160,16 +3159,16 @@ typedef struct PacManChars {
} pacmancharacters_t;
static void mode_pacman(void) {
- constexpr unsigned ORANGEYELLOW = 0xFFCC00;
- constexpr unsigned PURPLEISH = 0xB000B0;
- constexpr unsigned ORANGEISH = 0xFF8800;
- constexpr unsigned WHITEISH = 0x999999;
- constexpr unsigned PACMAN = 0; // PacMan is character[0]
+ constexpr uint32_t ORANGEYELLOW = 0xFFCC00;
+ constexpr uint32_t PURPLEISH = 0xB000B0;
+ constexpr uint32_t ORANGEISH = 0xFF8800;
+ constexpr uint32_t WHITEISH = 0x999999;
+ constexpr uint32_t PACMAN = 0; // PacMan is character[0]
constexpr uint32_t ghostColors[] = {RED, PURPLEISH, CYAN, ORANGEISH};
- unsigned maxPowerDots = min(SEGLEN / 10U, 255U); // cap the max so packed state fits in 8 bits
- unsigned numPowerDots = map(SEGMENT.intensity, 0, 255, 1, maxPowerDots);
- unsigned numGhosts = map(SEGMENT.custom3, 0, 31, 2, 8);
+ uint32_t maxPowerDots = min(SEGLEN / 10U, 255U); // cap the max so packed state fits in 8 bits
+ uint32_t numPowerDots = map(SEGMENT.intensity, 0, 255, 1, maxPowerDots);
+ uint32_t numGhosts = map(SEGMENT.custom3, 0, 31, 2, 8);
bool smearMode = SEGMENT.check2;
// Pack two 8-bit values into one 16-bit field (stored in SEGENV.aux0)
@@ -3178,7 +3177,7 @@ static void mode_pacman(void) {
SEGENV.aux0 = combined_value;
// Allocate segment data
- unsigned dataSize = sizeof(pacmancharacters_t) * (numGhosts + maxPowerDots + 1); // +1 is the PacMan character
+ uint32_t dataSize = sizeof(pacmancharacters_t) * (numGhosts + maxPowerDots + 1); // +1 is the PacMan character
if (SEGLEN <= 16 + (2*numGhosts) || !SEGENV.allocateData(dataSize)) FX_FALLBACK_STATIC;
pacmancharacters_t *character = reinterpret_cast(SEGENV.data);
@@ -3186,7 +3185,7 @@ static void mode_pacman(void) {
// On first call (or after settings change), `topPos` is not known yet, so fall back to the full segment length in that case.
int maxBlinkPos = (SEGENV.call == 0) ? (int)SEGLEN - 1 : character[PACMAN].topPos;
if (maxBlinkPos < 20) maxBlinkPos = 20;
- int startBlinkingGhostsLED = (SEGLEN < 64)
+ int startBlinkingGhostsLED = (SEGLEN < 64U)
? (int)SEGLEN / 3
: map(SEGMENT.custom1, 0, 255, 20, maxBlinkPos);
@@ -3200,19 +3199,19 @@ static void mode_pacman(void) {
character[PACMAN].blue = false;
// Initialize ghosts with alternating colors
- for (int i = 1; i <= numGhosts; i++) {
+ for (uint32_t i = 1; i <= numGhosts; i++) {
character[i].color = ghostColors[(i-1) % 4];
- character[i].pos = -2 * (i + 1);
+ character[i].pos = -2 * int32_t(i + 1);
character[i].direction = true;
character[i].blue = false;
}
// Initialize power dots
- for (int i = 0; i < numPowerDots; i++) {
+ for (uint32_t i = 0; i < numPowerDots; i++) {
character[i + numGhosts + 1].color = ORANGEYELLOW;
character[i + numGhosts + 1].eaten = false;
}
- character[numGhosts + 1].pos = SEGLEN - 1; // Last power dot at end
+ character[numGhosts + 1].pos = int32_t(SEGLEN - 1); // Last power dot at end
}
if (strip.now > SEGENV.step) {
@@ -3226,50 +3225,50 @@ static void mode_pacman(void) {
// Draw white dots in front of PacMan if option selected
if (SEGMENT.check1) {
int step = SEGMENT.check3 ? 1 : 2; // Compact or spaced dots
- for (int i = SEGLEN - 1; i > character[PACMAN].topPos; i -= step) {
+ for (int i = (int32_t)(SEGLEN - 1); i > character[PACMAN].topPos; i -= step) {
SEGMENT.setPixelColor(i, WHITEISH);
}
}
// Update power dot positions dynamically
uint32_t everyXLeds = (((uint32_t)SEGLEN - 10U) << 8) / numPowerDots; // Fixed-point spacing for power dots: use 32-bit math to avoid overflow on long segments.
- for (int i = 1; i < numPowerDots; i++) {
- character[i + numGhosts + 1].pos = 10 + ((i * everyXLeds) >> 8);
+ for (uint32_t i = 1; i < numPowerDots; i++) {
+ character[i + numGhosts + 1].pos = int32_t(10 + ((i * everyXLeds) >> 8));
}
// Blink power dots every 10 ticks
- if (SEGENV.aux1 % 10 == 0) {
+ if (SEGENV.aux1 % 10U == 0) {
uint32_t dotColor = (character[numGhosts + 1].color == ORANGEYELLOW) ? BLACK : ORANGEYELLOW;
- for (int i = 0; i < numPowerDots; i++) {
+ for (uint32_t i = 0; i < numPowerDots; i++) {
character[i + numGhosts + 1].color = dotColor;
}
}
// Blink blue ghosts when nearing start
- if (SEGENV.aux1 % 15 == 0 && character[1].blue && character[PACMAN].pos <= startBlinkingGhostsLED) {
+ if (SEGENV.aux1 % 15U == 0 && character[1].blue && character[PACMAN].pos <= startBlinkingGhostsLED) {
uint32_t ghostColor = (character[1].color == BLUE) ? WHITEISH : BLUE;
- for (int i = 1; i <= numGhosts; i++) {
+ for (uint32_t i = 1; i <= numGhosts; i++) {
character[i].color = ghostColor;
}
}
// Draw uneaten power dots
- for (int i = 0; i < numPowerDots; i++) {
- if (!character[i + numGhosts + 1].eaten && (unsigned)character[i + numGhosts + 1].pos < SEGLEN) {
+ for (uint32_t i = 0; i < numPowerDots; i++) {
+ if (!character[i + numGhosts + 1].eaten && (uint32_t)character[i + numGhosts + 1].pos < SEGLEN) {
SEGMENT.setPixelColor(character[i + numGhosts + 1].pos, character[i + numGhosts + 1].color);
}
}
// Check if PacMan ate a power dot
- for (int j = 0; j < numPowerDots; j++) {
+ for (uint32_t j = 0; j < numPowerDots; j++) {
auto &dot = character[j + numGhosts + 1];
if (character[PACMAN].pos == dot.pos && !dot.eaten) {
// Reverse all characters - PacMan now chases ghosts
- for (int i = 0; i <= numGhosts; i++) {
+ for (uint32_t i = 0; i <= numGhosts; i++) {
character[i].direction = false;
}
// Turn ghosts blue
- for (int i = 1; i <= numGhosts; i++) {
+ for (uint32_t i = 1; i <= numGhosts; i++) {
character[i].color = BLUE;
character[i].blue = true;
}
@@ -3281,17 +3280,17 @@ static void mode_pacman(void) {
// Reset when PacMan reaches start with blue ghosts
if (character[1].blue && character[PACMAN].pos <= 0) {
// Reverse direction back
- for (int i = 0; i <= numGhosts; i++) {
+ for (uint32_t i = 0; i <= numGhosts; i++) {
character[i].direction = true;
}
// Reset ghost colors
- for (int i = 1; i <= numGhosts; i++) {
+ for (uint32_t i = 1; i <= numGhosts; i++) {
character[i].color = ghostColors[(i-1) % 4];
character[i].blue = false;
}
// Reset power dots if last one was eaten
if (character[numGhosts + 1].eaten) {
- for (int i = 0; i < numPowerDots; i++) {
+ for (uint32_t i = 0; i < numPowerDots; i++) {
character[i + numGhosts + 1].eaten = false;
}
character[PACMAN].topPos = 0; // set the top position of PacMan to LED 0 (beginning of the segment)
@@ -3299,24 +3298,24 @@ static void mode_pacman(void) {
}
// Update and draw characters based on speed setting
- bool updatePositions = (SEGENV.aux1 % map(SEGMENT.speed, 0, 255, 15, 1) == 0);
+ bool updatePositions = (SEGENV.aux1 % uint32_t(map(SEGMENT.speed, 0, 255, 15, 1)) == 0);
// update positions of characters if it's time to do so
if (updatePositions) {
character[PACMAN].pos += character[PACMAN].direction ? 1 : -1;
- for (int i = 1; i <= numGhosts; i++) {
+ for (uint32_t i = 1; i <= numGhosts; i++) {
character[i].pos += character[i].direction ? 1 : -1;
}
}
// Draw PacMan
- if ((unsigned)character[PACMAN].pos < SEGLEN) {
+ if ((uint32_t)character[PACMAN].pos < SEGLEN) {
SEGMENT.setPixelColor(character[PACMAN].pos, character[PACMAN].color);
}
// Draw ghosts
- for (int i = 1; i <= numGhosts; i++) {
- if ((unsigned)character[i].pos < SEGLEN) {
+ for (uint32_t i = 1; i <= numGhosts; i++) {
+ if ((uint32_t)character[i].pos < SEGLEN) {
SEGMENT.setPixelColor(character[i].pos, character[i].color);
}
}
@@ -4183,14 +4182,14 @@ void mode_pacifica()
uint32_t nowOld = strip.now;
CRGBPalette16 pacifica_palette_1 =
- { 0x000507, 0x000409, 0x00030B, 0x00030D, 0x000210, 0x000212, 0x000114, 0x000117,
- 0x000019, 0x00001C, 0x000026, 0x000031, 0x00003B, 0x000046, 0x14554B, 0x28AA50 };
+ { 0x002229, 0x001E2F, 0x001934, 0x001938, 0x00143F, 0x001443, 0x00B047, 0x00B04C, // note: palettes are gamma inverted using gamma 2.0 to get closer to pre 16.0 looks
+ 0x00004F, 0x000054, 0x000062, 0x00006F, 0x00007A, 0x000085, 0x47938A, 0x64D08E };
CRGBPalette16 pacifica_palette_2 =
- { 0x000507, 0x000409, 0x00030B, 0x00030D, 0x000210, 0x000212, 0x000114, 0x000117,
- 0x000019, 0x00001C, 0x000026, 0x000031, 0x00003B, 0x000046, 0x0C5F52, 0x19BE5F };
+ { 0x002229, 0x001E2F, 0x001934, 0x001938, 0x00143F, 0x001443, 0x00B047, 0x00B04C,
+ 0x00004F, 0x000054, 0x000062, 0x00006F, 0x00007A, 0x000085, 0x369B90, 0x4FDC9B };
CRGBPalette16 pacifica_palette_3 =
- { 0x000208, 0x00030E, 0x000514, 0x00061A, 0x000820, 0x000927, 0x000B2D, 0x000C33,
- 0x000E39, 0x001040, 0x001450, 0x001860, 0x001C70, 0x002080, 0x1040BF, 0x2060FF };
+ { 0x00142C, 0x00193B, 0x002247, 0x002551, 0x002C5A, 0x002F63, 0x00346B, 0x003671,
+ 0x003B78, 0x003F7F, 0x00478E, 0x004D9C, 0x0054A9, 0x005AB4, 0x3F7FDC, 0x5A9CFF };
if (SEGMENT.palette) {
pacifica_palette_1 = SEGPALETTE;
@@ -4241,10 +4240,10 @@ void mode_pacifica()
c += CRGB(overage, overage2, qadd8(overage2, overage2));
}
- //deepen the blues and greens
- c.blue = scale8(c.blue, 145);
- c.green = scale8(c.green, 200);
- c |= CRGB( 2, 5, 7);
+ //deepen the blues and greens note: no longer needed with proper gamma in 16.0
+ //c.blue = scale8(c.blue, 145);
+ //c.green = scale8(c.green, 200);
+ //c |= CRGB( 2, 5, 7);
SEGMENT.setPixelColor(i, c);
}
@@ -4374,8 +4373,8 @@ void mode_noisepal(void) { // Slow noise pale
SEGENV.step = strip.now;
unsigned baseI = hw_random8();
- //palettes[1] = CRGBPalette16(CHSV(baseI+hw_random8(64), 255, hw_random8(128,255)), CHSV(baseI+128, 255, hw_random8(128,255)), CHSV(baseI+hw_random8(92), 192, hw_random8(128,255)), CHSV(baseI+hw_random8(92), 255, hw_random8(128,255)));
- palettes[1] = CRGBPalette16(CHSV(baseI+hw_random8(64), 255, hw_random8(128,255)), CHSV(baseI+128, 255, hw_random8(128,255)), CHSV(baseI+hw_random8(92), 192, hw_random8(128,255)), CHSV(baseI+hw_random8(92), 255, hw_random8(128,255)));
+ uint32_t minBri = gamma8inv(128); // use gamma inversion on min brightness value to restore pre 16.0 looks (more brilliant palettes)
+ palettes[1] = CRGBPalette16(CHSV(baseI+hw_random8(64), 255, hw_random8(minBri,255)), CHSV(baseI+128, 255, hw_random8(minBri,255)), CHSV(baseI+hw_random8(92), 192, hw_random8(minBri,255)), CHSV(baseI+hw_random8(92), 255, hw_random8(minBri,255)));
}
//EVERY_N_MILLIS(10) { //(don't have to time this, effect function is only called every 24ms)
@@ -4760,10 +4759,10 @@ void mode_tv_simulator(void) {
tvSimulator->actualColorB = temp[n ];
}
}
- // Apply gamma correction, further expand to 16/16/16
- nr = (uint8_t)gamma8(tvSimulator->actualColorR) * 257; // New R/G/B
- ng = (uint8_t)gamma8(tvSimulator->actualColorG) * 257;
- nb = (uint8_t)gamma8(tvSimulator->actualColorB) * 257;
+ // expand to 16 bit
+ nr = (uint8_t)(tvSimulator->actualColorR) * 257; // New R/G/B
+ ng = (uint8_t)(tvSimulator->actualColorG) * 257;
+ nb = (uint8_t)(tvSimulator->actualColorB) * 257;
if (SEGENV.aux0 == 0) { // initialize next iteration
SEGENV.aux0 = 1;
@@ -5721,7 +5720,7 @@ void mode_2Dmatrix(void) { // Matrix2D. By Jeremy Williams. Ada
SEGENV.step = 0;
}
- uint8_t fade = map(SEGMENT.custom1, 0, 255, 50, 250); // equals trail size
+ uint8_t fade = map(SEGMENT.custom1, 0, 255, 30, 250); // equals trail size
uint8_t speed = (256-SEGMENT.speed) >> map(min(rows, 150), 0, 150, 0, 3); // slower speeds for small displays
uint32_t spawnColor;
@@ -5730,8 +5729,8 @@ void mode_2Dmatrix(void) { // Matrix2D. By Jeremy Williams. Ada
spawnColor = SEGCOLOR(0);
trailColor = SEGCOLOR(1);
} else {
- spawnColor = RGBW32(175,255,175,0);
- trailColor = RGBW32(27,130,39,0);
+ spawnColor = RGBW32(gamma8inv(175), gamma8inv(255), gamma8inv(175), 0); // use gamma inversion to restor original pre 16.0 looks
+ trailColor = RGBW32(gamma8inv(27), gamma8inv(130), gamma8inv(39), 0);
}
bool emptyScreen = true;
@@ -6837,7 +6836,7 @@ void mode_gravcenter_base(unsigned mode) {
uint8_t gravity = 8 - SEGMENT.speed/32;
int offset = 1;
if(mode == 2) offset = 0; // Gravimeter
- if (tempsamp >= gravcen->topLED) gravcen->topLED = tempsamp-offset;
+ if (tempsamp >= gravcen->topLED + offset) gravcen->topLED = tempsamp-offset;
else if (gravcen->gravityCounter % gravity == 0) gravcen->topLED--;
if(mode == 1) { //Gravcentric
@@ -7006,8 +7005,8 @@ static const char _data_FX_MODE_MIDNOISE[] PROGMEM = "Midnoise@Fade rate,Max. le
//////////////////////
// I am the god of hellfire. . . Volume (only) reactive fire routine. Oh, look how short this is.
void mode_noisefire(void) { // Noisefire. By Andrew Tuline.
- CRGBPalette16 myPal = CRGBPalette16(CHSV(0,255,2), CHSV(0,255,4), CHSV(0,255,8), CHSV(0, 255, 8), // Fire palette definition. Lower value = darker.
- CHSV(0, 255, 16), CRGB::Red, CRGB::Red, CRGB::Red,
+ CRGBPalette16 myPal = CRGBPalette16(CHSV(0,255,20), CHSV(0,255,30), CHSV(0,255,40), CHSV(0, 255, 44), // Fire palette definition. Lower value = darker.
+ CHSV(0, 255, 64), CRGB::Red, CRGB::Red, CRGB::Red,
CRGB::DarkOrange, CRGB::DarkOrange, CRGB::Orange, CRGB::Orange,
CRGB::Yellow, CRGB::Orange, CRGB::Yellow, CRGB::Yellow);
@@ -7243,7 +7242,7 @@ void mode_DJLight(void) { // Written by ??? Adapted by Will Ta
if (SEGENV.aux0 != secondHand) { // Triggered millis timing.
SEGENV.aux0 = secondHand;
- CRGB color = CRGB(fftResult[15]/2, fftResult[5]/2, fftResult[0]/2); // 16-> 15 as 16 is out of bounds
+ CRGB color = CRGB(gamma8inv(fftResult[15]/2), gamma8inv(fftResult[5]/2), gamma8inv(fftResult[0]/2)); // apply gamma inversion to restor pre 16.0 looks
SEGMENT.setPixelColor(mid, color.fadeToBlackBy(map(fftResult[4], 0, 255, 255, 4))); // TODO - Update
// if SEGLEN equals 1 these loops won't execute
@@ -7323,7 +7322,7 @@ void mode_freqmatrix(void) { // Freqmatrix. By Andreas Pleschung.
uint8_t i = lowerLimit!=upperLimit ? map(FFT_MajorPeak, lowerLimit, upperLimit, 0, 255) : FFT_MajorPeak; // may under/overflow - so we enforce uint8_t
unsigned b = 255 * intensity;
if (b > 255) b = 255;
- color = CHSV(i, 240, (uint8_t)b); // implicit conversion to RGB supplied by FastLED
+ color = CHSV(i, 240, gamma8inv(b)); // use gamma inversion on brightness to restore pre 16.0 looks
}
// shift the pixels one pixel up
@@ -7414,7 +7413,7 @@ void mode_freqwave(void) { // Freqwave. By Andreas Pleschung.
int lowerLimit = 80 + 3 * SEGMENT.custom1;
uint8_t i = lowerLimit!=upperLimit ? map(FFT_MajorPeak, lowerLimit, upperLimit, 0, 255) : FFT_MajorPeak; // may under/overflow - so we enforce uint8_t
unsigned b = min(255.0f, 255.0f * intensity);
- color = CHSV(i, 240, (uint8_t)b); // implicit conversion to RGB supplied by FastLED
+ color = CHSV(i, 240, gamma8inv(b)); // use gamma inversion on brightness to restore pre 16.0 looks
}
SEGMENT.setPixelColor(SEGLEN/2, color);
@@ -7521,7 +7520,7 @@ void mode_waterfall(void) { // Waterfall. By: Andrew Tuline
unsigned k = SEGLEN-1;
if (samplePeak) {
- pixels[k] = (uint32_t)CRGB(CHSV(92,92,92));
+ pixels[k] = (uint32_t)CRGB(CHSV(92,92,gamma8inv(92))); // use gamma inversion on brightness to restore pre 16.0 looks
} else {
pixels[k] = color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(pixCol+SEGMENT.intensity, false, PALETTE_SOLID_WRAP, 0), (uint8_t)my_magnitude);
}
@@ -7632,7 +7631,7 @@ void mode_2DFunkyPlank(void) { // Written by ??? Adapted by Will Ta
int v = map(fftResult[band % 16], 0, 255, 10, 255);
for (int w = 0; w < barWidth; w++) {
int xpos = (barWidth * b) + w;
- SEGMENT.setPixelColorXY(xpos, 0, CHSV(hue, 255, v));
+ SEGMENT.setPixelColorXY(xpos, 0, CHSV(hue, 255, gamma8inv(v))); // use gamma inversion on brightness to restore original pre 16.0 looks
}
}
@@ -7981,7 +7980,7 @@ void mode_2Doctopus() {
byte radius = rMap[XY(x,y)].radius;
//CRGB c = CHSV(SEGENV.step / 2 - radius, 255, sin8_t(sin8_t((angle * 4 - radius) / 4 + SEGENV.step) + radius - SEGENV.step * 2 + angle * (SEGMENT.custom3/3+1)));
unsigned intensity = sin8_t(sin8_t((angle * 4 - radius) / 4 + SEGENV.step/2) + radius - SEGENV.step + angle * (SEGMENT.custom3/4+1));
- intensity = map((intensity*intensity) & 0xFFFF, 0, 65535, 0, 255); // add a bit of non-linearity for cleaner display
+ //intensity = map((intensity*intensity) & 0xFFFF, 0, 65535, 0, 255); // add a bit of non-linearity for cleaner display -> no longer needed with proper gamma correction
SEGMENT.setPixelColorXY(x, y, ColorFromPalette(SEGPALETTE, SEGENV.step / 2 - radius, intensity));
}
}
diff --git a/wled00/FX.h b/wled00/FX.h
index 9fd3a04d8a..a8f6e1cd76 100644
--- a/wled00/FX.h
+++ b/wled00/FX.h
@@ -42,7 +42,7 @@
#define DEFAULT_MODE (uint8_t)0
#define DEFAULT_SPEED (uint8_t)128
#define DEFAULT_INTENSITY (uint8_t)128
-#define DEFAULT_COLOR (uint32_t)0xFFAA00
+#define DEFAULT_COLOR (uint32_t)0xFFA000
#define DEFAULT_C1 (uint8_t)128
#define DEFAULT_C2 (uint8_t)128
#define DEFAULT_C3 (uint8_t)16
@@ -562,7 +562,7 @@ class Segment {
public:
Segment(uint16_t sStart=0, uint16_t sStop=30, uint16_t sStartY = 0, uint16_t sStopY = 1)
- : colors{DEFAULT_COLOR,BLACK,BLACK}
+ : colors{BLACK,BLACK,BLACK} // set colors to black, will be updated to orange if segment is created as "auto segment" or from UI
, start(sStart)
, stop(sStop > sStart ? sStop : sStart+1) // minimum length is 1
, startY(sStartY)
@@ -618,6 +618,7 @@ class Segment {
DEBUGFX_PRINTLN();
#endif
clearName();
+ stopTransition(); // deallocate "_t" (transition) and with it "_segOld" note: _segOld has _t=null, see copy constructor
#ifdef WLED_ENABLE_GIF
endImagePlayback(this);
#endif
@@ -879,6 +880,7 @@ class WS2812FX {
printSize(), // prints memory usage for strip components
#endif
finalizeInit(), // initialises strip components
+ updatePixelBuffer(), // (re)allocate memory for _pixels[]
service(), // executes effect functions when due and calls strip.show()
setCCT(uint16_t k), // sets global CCT (either in relative 0-255 value or in K)
setBrightness(uint8_t b, bool direct = false), // sets strip brightness
diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp
index cbd937fb73..5458c97488 100644
--- a/wled00/FX_fcn.cpp
+++ b/wled00/FX_fcn.cpp
@@ -98,7 +98,7 @@ Segment& Segment::operator= (const Segment &orig) {
if (this != &orig) {
// clean destination
if (name) { p_free(name); name = nullptr; }
- if (_t) stopTransition(); // also erases _t
+ stopTransition(); // delete _t
deallocateData();
p_free(pixels);
pixels = nullptr;
@@ -131,7 +131,7 @@ Segment& Segment::operator= (Segment &&orig) noexcept {
//DEBUG_PRINTF_P(PSTR("-- Moving segment: %p -> %p\n"), &orig, this);
if (this != &orig) {
if (name) { p_free(name); name = nullptr; } // free old name
- if (_t) stopTransition(); // also erases _t
+ stopTransition(); // delete _t
deallocateData(); // free old runtime data
p_free(pixels); // free old pixel buffer
// move source data
@@ -290,6 +290,8 @@ void Segment::loadPalette(CRGBPalette16 &targetPalette, uint8_t pal) {
}
// starting a transition has to occur before change so we get current values 1st
+// note: _t is the temporary segment that holds the values transitioned from (palette, colors, brightness,...) and the current segment holds the "to" values
+// if this is a non FADE transition or an FX change, the _oldSegment is created which is a full copy of the segment before the change
void Segment::startTransition(uint16_t dur, bool segmentCopy) {
if (dur == 0 || !isActive()) {
if (isInTransition()) _t->_dur = 0;
@@ -299,15 +301,42 @@ void Segment::startTransition(uint16_t dur, bool segmentCopy) {
if (segmentCopy && !_t->_oldSegment) {
// already in transition but segment copy requested and not yet created
_t->_oldSegment = new(std::nothrow) Segment(*this); // store/copy current segment settings
- _t->_start = millis(); // restart countdown
+ _t->_start = millis(); // restart transition timer
_t->_dur = dur;
- _t->_prevPaletteBlends = 0;
+ _t->_prevPaletteBlends = 0; // reset palette blends
if (_t->_oldSegment) {
- _t->_oldSegment->palette = _t->_palette; // restore original palette and colors (from start of transition)
+ _t->_oldSegment->palette = _t->_palette; // restore original palette, colors, brightness and CCT (from start of transition)
for (unsigned i = 0; i < NUM_COLORS; i++) _t->_oldSegment->colors[i] = _t->_colors[i];
+ _t->_oldSegment->opacity = _t->_bri;
+ _t->_oldSegment->cct = _t->_cct;
+ // if already partway through a FADE transition, set old segment's colors to current blend to avoid jumping back to original colors
+ if (_t->_progress > 0) {
+ // already in a transition, see comment below
+ for (unsigned i = 0; i < NUM_COLORS; i++) _t->_oldSegment->colors[i] = color_blend16(_t->_colors[i], colors[i], _t->_progress);
+ _t->_oldSegment->opacity = currentBri(); // update "original" brightness note: _t->_progress is updated in updateTransitionProgress() so still valid here
+ _t->_oldSegment->cct = currentCCT(); // update "original" CCT (reduces jump)
+ }
DEBUGFX_PRINTF_P(PSTR("-- Updated transition with segment copy: S=%p T(%p) O[%p] OP[%p]\n"), this, _t, _t->_oldSegment, _t->_oldSegment->pixels);
if (!_t->_oldSegment->isActive()) stopTransition();
}
+ } else if (_t->_progress > 0) {
+ // already in a transition: capture the current visual blend as the new "from" state so the incoming change does not cause a visible jump.
+ // _palT already holds the intermediate blended palette and will continue blending toward the new target (see beginDraw()), so no palette action needed.
+ // initial version by @blazoncek (https://github.com/blazoncek/WLED/commit/40d9812)
+ for (unsigned i = 0; i < NUM_COLORS; i++) _t->_colors[i] = color_blend16(_t->_colors[i], colors[i], _t->_progress);
+ _t->_bri = currentBri(); // update "original" brightness note: _t->_progress is updated in updateTransitionProgress() so still valid here
+ _t->_cct = currentCCT(); // update "original" CCT (reduces jump)
+ // restart transition timer only if a pure FADE transition, otherwise let the FX change or non-FADE transition finish
+ // this avoids a re-start of the transition if color or brightness is changed during an ongoing FX or non-FADE transition
+ if (blendingStyle == TRANSITION_FADE) {
+ if (_t->_oldSegment != nullptr) {
+ if (_t->_oldSegment->mode != mode)
+ return; // do not reset transition if this is an FX change, note: the disadvantage is that colors still jump in that case
+ }
+ _t->_start = millis();
+ _t->_dur = dur;
+ _t->_prevPaletteBlends = 0;
+ }
}
return;
}
@@ -333,6 +362,7 @@ void Segment::startTransition(uint16_t dur, bool segmentCopy) {
}
void Segment::stopTransition() {
+ if (_t == nullptr) return; // no ongoing transition
DEBUG_PRINTF_P(PSTR("-- Stopping transition: S=%p T(%p) O[%p]\n"), this, _t, _t->_oldSegment);
delete _t;
_t = nullptr;
@@ -365,7 +395,7 @@ uint8_t Segment::currentBri() const {
if (prog < 0xFFFFU) {
// this will blend opacity in new mode if style is FADE (single effect call)
if (blendingStyle == TRANSITION_FADE) curBri = (prog * curBri + _t->_bri * (0xFFFFU - prog)) / 0xFFFFU;
- else curBri = Segment::isPreviousMode() ? _t->_bri : curBri;
+ else curBri = Segment::isPreviousMode() ? _t->_bri : curBri;
}
return curBri;
}
@@ -452,7 +482,7 @@ void Segment::setGeometry(uint16_t i1, uint16_t i2, uint8_t grp, uint8_t spc, ui
DEBUGFX_PRINTF_P(PSTR("Segment geometry: %d,%d -> %d,%d [%d,%d]\n"), (int)i1, (int)i2, (int)i1Y, (int)i2Y, (int)grp, (int)spc);
markForReset();
- if (_t) stopTransition(); // we can't use transition if segment dimensions changed
+ stopTransition(); // we can't use transition if segment dimensions changed
stateChanged = true; // send UDP/WS broadcast
// apply change immediately
@@ -1117,11 +1147,11 @@ void Segment::blur(uint8_t blur_amount, bool smear) const {
* Rotates the color in HSV space, where pos is H. (0=0deg, 256=360deg)
*/
uint32_t Segment::color_wheel(uint8_t pos) const {
- if (palette) return color_from_palette(pos, false, false, 0); // only wrap if "always wrap" is set
- uint8_t w = W(getCurrentColor(0));
+ if (palette) return color_from_palette(pos, false, true, 0); // color_wheel is a continuous (moving) wheel, so wrap end->start (restores pre-0.16 behaviour)
CRGBW rgb;
rgb = CHSV32(static_cast(pos << 8), 255, 255);
- return rgb.color32 | (w << 24); // add white channel
+ rgb.w = W(getCurrentColor(0)); // add white channel
+ return rgb.color32;
}
/*
@@ -1272,11 +1302,17 @@ void WS2812FX::finalizeInit() {
deserializeMap(); // (re)load default ledmap (will also setUpMatrix() if ledmap does not exist)
// allocate frame buffer after matrix has been set up (gaps!)
+ updatePixelBuffer();
+ DEBUG_PRINTF_P(PSTR("Heap after strip init: %uB\n"), getFreeHeapSize());
+}
+
+// update global _pixels[] buffer to match getLengthTotal() note: if allocation fails, WLED will not render anything
+void WS2812FX::updatePixelBuffer() {
+ uint32_t requiredMem = getLengthTotal() * sizeof(uint32_t);
p_free(_pixels); // using realloc on large buffers can cause additional fragmentation instead of reducing it
// use PSRAM if available: there is no measurable perfomance impact between PSRAM and DRAM on S2/S3 with QSPI PSRAM for this buffer
- _pixels = static_cast(allocate_buffer(getLengthTotal() * sizeof(uint32_t), BFRALLOC_ENFORCE_PSRAM | BFRALLOC_NOBYTEACCESS | BFRALLOC_CLEAR));
- DEBUG_PRINTF_P(PSTR("strip buffer size: %uB\n"), getLengthTotal() * sizeof(uint32_t));
- DEBUG_PRINTF_P(PSTR("Heap after strip init: %uB\n"), getFreeHeapSize());
+ _pixels = static_cast(allocate_buffer(requiredMem, BFRALLOC_ENFORCE_PSRAM | BFRALLOC_NOBYTEACCESS | BFRALLOC_CLEAR));
+ DEBUG_PRINTF_P(PSTR("strip buffer size: %uB\n"), requiredMem);
}
void WS2812FX::service() {
@@ -1606,7 +1642,9 @@ void WS2812FX::blendSegment(const Segment &topSegment) const {
// workaround for On/Off transition
// (bri != briT) && !bri => from On to Off
// (bri != briT) && bri => from Off to On
- if ((briOld == 0 || bri == 0) && ((!clipped && (bri != briT) && !bri) || (clipped && (bri != briT) && bri))) c_a = BLACK;
+ // note: only blank pixels once the segment transition has actually started; bri changes before
+ // startTransition() is called (stateUpdated()) and a frame rendered in that window would blank the whole segment
+ if (topSegment.isInTransition() && (briOld == 0 || bri == 0) && ((!clipped && (bri != briT) && !bri) || (clipped && (bri != briT) && bri))) c_a = BLACK;
}
// map it into frame buffer
x = c; // restore coordiates if we were PUSHing
@@ -1678,7 +1716,9 @@ void WS2812FX::blendSegment(const Segment &topSegment) const {
// workaround for On/Off transition
// (bri != briT) && !bri => from On to Off
// (bri != briT) && bri => from Off to On
- if ((briOld == 0 || bri == 0) && ((!clipped && (bri != briT) && !bri) || (clipped && (bri != briT) && bri))) c_a = BLACK;
+ // note: only blank pixels once the segment transition has actually started; bri changes before
+ // startTransition() is called (stateUpdated()) and a frame rendered in that window would blank the whole segment
+ if (topSegment.isInTransition() && (briOld == 0 || bri == 0) && ((!clipped && (bri != briT) && !bri) || (clipped && (bri != briT) && bri))) c_a = BLACK;
}
// map into frame buffer
i = k; // restore index if we were PUSHing
@@ -1730,6 +1770,9 @@ void WS2812FX::show() {
int oldCCT = Bus::getCCT(); // store original CCT value (since it is global)
// when cctFromRgb is true we implicitly calculate WW and CW from RGB values (cct==-1)
if (cctFromRgb) BusManager::setSegmentCCT(-1);
+ // use color gamma correction if enabled, not in realtime mode with gamma disabled or currently overriding RT mode
+ bool useGammaCorrection = gammaCorrectCol && !(realtimeMode && arlsDisableGammaCorrection && !realtimeOverride);
+
for (size_t i = 0; i < totalLen; i++) {
// when correctWB is true setSegmentCCT() will convert CCT into K with which we can then
// correct/adjust RGB value according to desired CCT value, it will still affect actual WW/CW ratio
@@ -1738,8 +1781,8 @@ void WS2812FX::show() {
}
uint32_t c = _pixels[i]; // need a copy, do not modify _pixels directly (no byte access allowed on ESP32)
- if (c > 0 && !(realtimeMode && arlsDisableGammaCorrection))
- c = gamma32(c); // apply gamma correction if enabled note: applying gamma after brightness has too much color loss
+ if (c > 0 && useGammaCorrection)
+ c = gamma32(c); // apply gamma correction if enabled note: applying gamma after brightness has too much color loss
BusManager::setPixelColor(getMappedPixelIndex(i), c);
}
Bus::setCCT(oldCCT); // restore old CCT for ABL adjustments
@@ -1925,8 +1968,9 @@ void WS2812FX::resetSegments() {
if (isServicing()) return;
_segments.clear(); // destructs all Segment as part of clearing
_segments.emplace_back(0, isMatrix ? Segment::maxWidth : _length, 0, isMatrix ? Segment::maxHeight : 1);
- if(_segments.size() == 0) {
- _segments.emplace_back(); // if out of heap, create a default segment
+ if (getActiveSegmentsNum() == 0) {
+ _segments.clear(); // free failed segment
+ _segments.emplace_back(); // if out of heap, create a default 30 pixel segment
errorFlag = ERR_NORAM_PX;
}
_segments.shrink_to_fit(); // just in case ...
@@ -1985,6 +2029,9 @@ void WS2812FX::makeAutoSegments(bool forceReset) {
for (size_t i = 1; i < s; i++) {
_segments.emplace_back(segStarts[i], segStops[i]);
}
+ for (size_t i = 0; i < _segments.size(); i++) {
+ _segments[i].colors[0] = DEFAULT_COLOR; // set color to default orange on all segments
+ }
DEBUGFX_PRINTF_P(PSTR("%d auto segments created.\n"), _segments.size());
} else {
@@ -2000,6 +2047,7 @@ void WS2812FX::makeAutoSegments(bool forceReset) {
#endif
}
}
+ if (getActiveSegmentsNum() == 0) resetSegments(); // fallback if auto segment creation failed
_mainSegment = 0;
fixInvalidSegments();
@@ -2080,10 +2128,13 @@ bool WS2812FX::deserializeMap(unsigned n) {
customMappingSize = 0; // prevent use of mapping if anything goes wrong
currentLedmap = 0;
if (n == 0 || isFile) interfaceUpdateCallMode = CALL_MODE_WS_SEND; // schedule WS update (to inform UI)
+ uint32_t lengthTotalBefore = strip.getLengthTotal();
if (!isFile && n==0 && isMatrix) {
// 2D panel support creates its own ledmap (on the fly) if a ledmap.json does not exist
setUpMatrix();
+ if (strip.getLengthTotal() != lengthTotalBefore)
+ strip.updatePixelBuffer(); // allocate _pixels[] to match new length
return false;
}
@@ -2131,6 +2182,7 @@ bool WS2812FX::deserializeMap(unsigned n) {
int index = atoi(number);
if (index < 0 || index > 65535) index = 0xFFFF; // prevent integer wrap around
customMappingTable[customMappingSize++] = index;
+ if (end != nullptr) break; // array closing ']' was in this chunk; stop before atoi() coerces trailing JSON keys into bogus entries
if (customMappingSize >= getLengthTotal()) break;
} else break; // there was nothing to read, stop
}
@@ -2158,6 +2210,8 @@ bool WS2812FX::deserializeMap(unsigned n) {
}
releaseJSONBufferLock();
+ if (strip.getLengthTotal() != lengthTotalBefore)
+ strip.updatePixelBuffer(); // allocate _pixels[] to match new length
return (customMappingSize > 0);
}
diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp
index 175d49ba9c..1ba808d8b0 100644
--- a/wled00/bus_manager.cpp
+++ b/wled00/bus_manager.cpp
@@ -351,26 +351,25 @@ void BusDigital::setColorOrder(uint8_t colorOrder) {
// credit @willmmiles & @netmindz https://github.com/wled/WLED/pull/4056
std::vector BusDigital::getLEDTypes() {
return {
- {TYPE_WS2812_RGB, "D", PSTR("WS281x")},
+ {TYPE_WS2812_RGB, "D", PSTR("WS281x RGB")},
+ {TYPE_WS2811_400KHZ, "D", PSTR("400kHz RGB")},
+ {TYPE_TM1829, "D", PSTR("TM1829 RGB")},
+ {TYPE_UCS8903, "D", PSTR("UCS8903 RGB")},
+ {TYPE_APA106, "D", PSTR("APA106/PL9823 RGB")},
+ {TYPE_TM1914, "D", PSTR("TM1914 RGB")},
{TYPE_SK6812_RGBW, "D", PSTR("SK6812/WS2814 RGBW")},
- {TYPE_TM1814, "D", PSTR("TM1814")},
- {TYPE_WS2811_400KHZ, "D", PSTR("400kHz")},
- {TYPE_TM1829, "D", PSTR("TM1829")},
- {TYPE_UCS8903, "D", PSTR("UCS8903")},
- {TYPE_APA106, "D", PSTR("APA106/PL9823")},
- {TYPE_TM1914, "D", PSTR("TM1914")},
- {TYPE_FW1906, "D", PSTR("FW1906 GRBCW")},
{TYPE_UCS8904, "D", PSTR("UCS8904 RGBW")},
- {TYPE_WS2805, "D", PSTR("WS2805 RGBCW")},
- {TYPE_SM16825, "D", PSTR("SM16825 RGBCW")},
+ {TYPE_TM1814, "D", PSTR("TM1814 RGBW")},
+ {TYPE_FW1906, "D", PSTR("FW1906/WS2811 RGBCCT")},
+ {TYPE_WS2805, "D", PSTR("WS2805 RGBCCT")},
+ {TYPE_SM16825, "D", PSTR("SM16825 RGBCCT")},
{TYPE_WS2812_1CH_X3, "D", PSTR("WS2811 White")},
- //{TYPE_WS2812_2CH_X3, "D", PSTR("WS281x CCT")}, // not implemented
{TYPE_WS2812_WWA, "D", PSTR("WS281x WWA")}, // amber ignored
- {TYPE_WS2801, "2P", PSTR("WS2801")},
- {TYPE_APA102, "2P", PSTR("APA102")},
- {TYPE_LPD8806, "2P", PSTR("LPD8806")},
- {TYPE_LPD6803, "2P", PSTR("LPD6803")},
- {TYPE_P9813, "2P", PSTR("PP9813")},
+ {TYPE_WS2801, "2P", PSTR("WS2801 RGB")},
+ {TYPE_APA102, "2P", PSTR("APA102 RGB")},
+ {TYPE_LPD8806, "2P", PSTR("LPD8806 RGB")},
+ {TYPE_LPD6803, "2P", PSTR("LPD6803 RGB")},
+ {TYPE_P9813, "2P", PSTR("P9813 RGB")},
};
}
@@ -611,7 +610,7 @@ std::vector BusPwm::getLEDTypes() {
{TYPE_ANALOG_2CH, "AA", PSTR("PWM CCT")},
{TYPE_ANALOG_3CH, "AAA", PSTR("PWM RGB")},
{TYPE_ANALOG_4CH, "AAAA", PSTR("PWM RGBW")},
- {TYPE_ANALOG_5CH, "AAAAA", PSTR("PWM RGB+CCT")},
+ {TYPE_ANALOG_5CH, "AAAAA", PSTR("PWM RGBCCT")},
//{TYPE_ANALOG_6CH, "AAAAAA", PSTR("PWM RGB+DCCT")}, // unimplementable ATM
};
}
@@ -802,17 +801,22 @@ BusHub75Matrix::BusHub75Matrix(const BusConfig &bc) : Bus(bc.type, bc.start, bc.
_hasRgb = true;
_hasWhite = false;
virtualDisp = nullptr; // todo: this should be solved properly, can cause memory leak (if omitted here, nothing seems to work)
+ _isVirtual = false;
+ _isQuadScan = false;
// aliases for easier reading
- uint8_t panelWidth = bc.pins[0];
- uint8_t panelHeight = bc.pins[1];
- uint8_t chainLength = bc.pins[2];
+ unsigned panelWidth = bc.pins[0];
+ unsigned panelHeight = bc.pins[1];
+ unsigned chainLength = bc.pins[2];
_rows = bc.pins[3];
_cols = bc.pins[4];
+ unsigned physicalPanelWidth = max(16U, min(128U, panelWidth)); // keep a copy because QS panels require modified width/height
+ unsigned physicalPanelHeight = max(16U, min(64U, panelHeight));
mxconfig.double_buff = false; // Use our own memory-optimised buffer rather than the driver's own double-buffer
// mxconfig.driver = HUB75_I2S_CFG::ICN2038S; // experimental - use specific shift register driver
// mxconfig.driver = HUB75_I2S_CFG::FM6124; // try this driver in case you panel stays dark, or when colors look too pastel
+ // Other possible shiftreg drivers: HUB75_I2S_CFG::FM6126A, HUB75_I2S_CFG::ICN2038S, HUB75_I2S_CFG::MBI5124, HUB75_I2S_CFG::DP3246
// mxconfig.latch_blanking = 3;
// mxconfig.i2sspeed = HUB75_I2S_CFG::HZ_10M; // experimental - 5MHZ should be enugh, but colours looks slightly better at 10MHz
@@ -821,24 +825,31 @@ BusHub75Matrix::BusHub75Matrix(const BusConfig &bc) : Bus(bc.type, bc.start, bc.
mxconfig.clkphase = bc.reversed;
// allow chain length up to 4, limit to prevent bad data from preventing boot due to low memory
- mxconfig.chain_length = max((uint8_t) 1, min(chainLength, (uint8_t) 4));
+ mxconfig.chain_length = max(1U, min(chainLength, 4U));
- if (mxconfig.mx_height >= 64 && (mxconfig.chain_length > 1)) {
- DEBUGBUS_PRINTLN(F("WARNING, only single panel can be used of 64 pixel boards due to memory"));
- mxconfig.chain_length = 1;
+ if (panelHeight >= 64 && (mxconfig.chain_length > 1)) { // need to check panelHeight; mxconfig.mx_height not assigned yet
+ #if defined(BOARD_HAS_PSRAM) // limitation to one panel only applies to boards without PSRAM
+ if (!psramFound() || ESP.getPsramSize() == 0) // PSRAM sanity check
+ #endif
+ {
+ DEBUGBUS_PRINTLN(F("WARNING, only single panel can be used of 64 pixel boards due to memory"));
+ mxconfig.chain_length = 1;
+ }
}
if (bc.type == TYPE_HUB75MATRIX_HS) {
- mxconfig.mx_width = min((uint8_t) 64, panelWidth); // TODO: UI limit is 128, this limits to 64
- mxconfig.mx_height = min((uint8_t) 64, panelHeight);
+ mxconfig.mx_width = min(128U, panelWidth); // UI limit is 128
+ mxconfig.mx_height = min(64U, panelHeight);
} else if (bc.type == TYPE_HUB75MATRIX_QS) {
_isVirtual = true;
- mxconfig.mx_width = min((uint8_t) 64, panelWidth) * 2;
- mxconfig.mx_height = min((uint8_t) 64, panelHeight) / 2;
+ mxconfig.mx_width = min(128U, panelWidth) * 2;
+ mxconfig.mx_height = min(64U, panelHeight) / 2;
+ mxconfig.driver = HUB75_I2S_CFG::FM6124; // use FM6124 for "outdoor" 4-scan panels - workaround until we can make the driver user-configurable
} else {
DEBUGBUS_PRINTLN("Unknown type");
return;
}
+ _isQuadScan = (bc.type == TYPE_HUB75MATRIX_QS);
#if defined(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32S2)// classic esp32, or esp32-s2: reduce bitdepth for large panels
if (mxconfig.mx_height >= 64) {
@@ -849,49 +860,94 @@ BusHub75Matrix::BusHub75Matrix(const BusConfig &bc) : Bus(bc.type, bc.start, bc.
#endif
-
// 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};
-#if defined(ARDUINO_ADAFRUIT_MATRIXPORTAL_ESP32S3) // MatrixPortal ESP32-S3
-
+#if defined(ARDUINO_ADAFRUIT_MATRIXPORTAL_ESP32S3) || defined(MATRIXPORTAL_S3_PINOUT) // MatrixPortal ESP32-S3
// https://www.adafruit.com/product/5778
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)
-
+#elif defined(HD_WF2_PINOUT) || defined(HD_WF2_S3_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
+#elif defined(HD_WF1_PINOUT) || defined(HD_WF1_S2_PINOUT) || defined(CONFIG_IDF_TARGET_ESP32S2)
+ #warning "using HUB75 on esp32-s2 in not recommended due to stability problems and low RAM"
+ // Huidu HD-WF1 ESP32-S2 - not recommended !
+ // https://github.com/mrcodetastic/ESP32-HUB75-MatrixPanel-DMA/issues/433
+ USER_PRINTLN("MatrixPanel_I2S_DMA - HD-WF1 S2 config");
+ mxconfig.gpio = {2, 6, 3, 4, 8, 5, 33, 35, 34, 39, 38, 37, 36, 12};
-#if defined(MOONHUB_S3_PINOUT)
- DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA - T7 S3 with PSRAM, MOONHUB pinout");
+#elif defined(CONFIG_IDF_TARGET_ESP32S3)
+ // specific ESP32-S3 pinouts
+ #if defined(MOONHUB_S3_PINOUT)
+ DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA - T7 S3, MOONHUB pinout");
// 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 = { 1, 5, 6, 7, 13, 9, 16, 48, 47, 21, 38, 8, 4, 18 };
-#else
- DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA - S3 with PSRAM");
+ #elif defined(WAVESHARE_S3_PINOUT)
+ DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA - Waveshare S3, Waveshare pinout");
+ // 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 = {4, 5, 6, 7, 15, 16, 18, 8, 3, 42, 9, 40, 2, 41};
+
+ #elif defined(SEENGREAT_V1_S3_PINOUT)
+ DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA - S3 devKit-C, SEENGREAT_V1 pinout");
+ // https://seengreat.com/wiki/186
+ mxconfig.gpio = { 37, 6, 36, // R1_PIN, G1_PIN, B1_PIN,
+ 35, 5, 0, // R2_PIN, G2_PIN, B2_PIN,
+ 45, 1, 48, 2, 4, // A_PIN, B_PIN, C_PIN, D_PIN, E_PIN,
+ 38, 21, 47 }; // LAT_PIN, OE_PIN,CLK_PIN
+
+ #elif defined(SEENGREAT_V2_S3_PINOUT)
+ DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA - S3 devKit-C, SEENGREAT_V2 pinout");
+ // https://seengreat.com/wiki/186
+ mxconfig.gpio = { 18, 8, 17, // R1_PIN, G1_PIN, B1_PIN,
+ 16, 1, 15, // R2_PIN, G2_PIN, B2_PIN,
+ 7, 48, 6, 47, 2, // A_PIN, B_PIN, C_PIN, D_PIN, E_PIN,
+ 21, 4, 5 }; // LAT_PIN, OE_PIN,CLK_PIN
+
+ #else
+ DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA - S3 generic pinout");
// 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 = {1, 2, 42, 41, 40, 39, 45, 48, 47, 21, 38, 8, 3, 18};
-#endif
-#elif defined(ESP32_FORUM_PINOUT) // Common format for boards designed for SmartMatrix
+ #endif // CONFIG_IDF_TARGET_ESP32S3
+#elif defined(CONFIG_IDF_TARGET_ESP32)
+ // generic ESP32 pinouts
+ #if defined(BOARD_HAS_PSRAM) // all ESP32 pinouts require gpio 16 or 17, which are controling PSRAM
+ #warning "ESP32 HUB75 pinout is not compatible with PSRAM boards."
+ #endif
+ #if defined(ESP32_FORUM_PINOUT) || defined(FORUM_ESP32_PINOUT) // Common format for boards designed for SmartMatrix
DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA - ESP32_FORUM_PINOUT");
/*
ESP32 with SmartMatrix's default pinout - ESP32_FORUM_PINOUT
https://github.com/pixelmatix/SmartMatrix/blob/teensylc/src/MatrixHardware_ESP32_V0.h
Can use a board like https://github.com/rorosaurus/esp32-hub75-driver
*/
-
mxconfig.gpio = { 2, 15, 4, 16, 27, 17, 5, 18, 19, 21, 12, 26, 25, 22 };
-#else
- DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA - Default pins");
+ #elif defined(SEENGREAT_V1_ESP32_PINOUT)
+ DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA - EP32-DevKitC V4, SEENGREAT_V1 pinout");
+ // https://seengreat.com/wiki/186
+ mxconfig.gpio = { 18, 25, 5, // R1_PIN, G1_PIN, B1_PIN,
+ 17, 33, 16, // R2_PIN, G2_PIN, B2_PIN,
+ 4, 3, 0, 21, 32, // A_PIN, B_PIN, C_PIN, D_PIN, E_PIN,
+ 19, 15, 2}; // LAT_PIN, OE_PIN,CLK_PIN
+
+ #elif defined(SEENGREAT_V2_ESP32_PINOUT)
+ DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA - EP32-DevKitC V4, SEENGREAT_V2 pinout, Latch pin IO2");
+ // https://seengreat.com/wiki/186
+ mxconfig.gpio = { 18, 17, 19, // R1_PIN, G1_PIN, B1_PIN,
+ 21, 23, 27, // R2_PIN, G2_PIN, B2_PIN,
+ 26, 16, 25, 4, 22, // A_PIN, B_PIN, C_PIN, D_PIN, E_PIN,
+ 2, 32, 33}; // LAT_PIN, OE_PIN,CLK_PIN
+
+ #else
+ DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA - ESP32 Default pins");
/*
https://github.com/mrfaptastic/ESP32-HUB75-MatrixPanel-DMA?tab=readme-ov-file
@@ -901,8 +957,11 @@ BusHub75Matrix::BusHub75Matrix(const BusConfig &bc) : Bus(bc.type, bc.start, bc.
https://www.electrodragon.com/product/rgb-matrix-panel-drive-interface-board-for-esp32-dma/
*/
- mxconfig.gpio = { 25, 26, 27, 14, 12, 13, 23, 19, 5, 17, 18, 4, 15, 16 };
+ mxconfig.gpio = { 25, 26, 27, 14, 12, 13, 23, 19, 5, 17, 18, 4, 15, 16 };
+ #endif // CONFIG_IDF_TARGET_ESP32
+ #else
+ #error "unknown or unsupported HUB75 board."
#endif
int8_t pins[PIN_COUNT];
@@ -934,7 +993,7 @@ BusHub75Matrix::BusHub75Matrix(const BusConfig &bc) : Bus(bc.type, bc.start, bc.
mxconfig.gpio.a, mxconfig.gpio.b, mxconfig.gpio.c, mxconfig.gpio.d, mxconfig.gpio.e, mxconfig.gpio.lat, mxconfig.gpio.oe, mxconfig.gpio.clk);
// OK, now we can create our matrix object
- display = new MatrixPanel_I2S_DMA(mxconfig);
+ display = new(std::nothrow) MatrixPanel_I2S_DMA(mxconfig);
if (display == nullptr) {
DEBUGBUS_PRINTLN("****** MatrixPanel_I2S_DMA !KABOOM! driver allocation failed ***********");
DEBUGBUS_PRINT(F("heap usage: ")); DEBUGBUS_PRINTLN(lastHeap - ESP.getFreeHeap());
@@ -990,28 +1049,30 @@ BusHub75Matrix::BusHub75Matrix(const BusConfig &bc) : Bus(bc.type, bc.start, bc.
}
setBitArray(_ledsDirty, _len, false); // reset dirty bits
- if (mxconfig.double_buff == false) {
- // create LEDs buffer (initialized to BLACK), prefer DRAM if enough heap is available (faster in case global _pixels buffer is in PSRAM as not both will fit the cache)
- _ledBuffer = static_cast(allocate_buffer(_len * sizeof(CRGB), BFRALLOC_PREFER_DRAM | BFRALLOC_CLEAR));
- }
+ // create LEDs buffer (initialized to BLACK), prefer DRAM if enough heap is available (faster in case global _pixels buffer is in PSRAM as not both will fit the cache)
+ _ledBuffer = static_cast(allocate_buffer(_len * sizeof(CRGB), BFRALLOC_PREFER_DRAM | BFRALLOC_CLEAR));
}
PANEL_CHAIN_TYPE chainType = CHAIN_NONE; // default for quarter-scan panels that do not use chaining
+ if (mxconfig.chain_length > 1 && (_rows > 1 || _cols > 1)) chainType = CHAIN_TOP_RIGHT_DOWN; // we need to use a _DOWN chainType, otherwise the first panel is upside-down
// chained panels with cols and rows define need the virtual display driver, so do quarter-scan panels
- if (chainLength > 1 && (_rows > 1 || _cols > 1) || bc.type == TYPE_HUB75MATRIX_QS) {
+ if (chainType != CHAIN_NONE || bc.type == TYPE_HUB75MATRIX_QS) {
_isVirtual = true;
- chainType = CHAIN_BOTTOM_LEFT_UP; // TODO: is there any need to support other chaining types?
- DEBUGBUS_PRINTF_P(PSTR("Using virtual matrix: %ux%u panels of %ux%u pixels\n"), _cols, _rows, mxconfig.mx_width, mxconfig.mx_height);
+ DEBUGBUS_PRINTF_P(PSTR("Using virtual matrix: %ux%u panels of %ux%u pixels\n"), _cols, _rows, physicalPanelWidth, physicalPanelHeight);
}
else {
_isVirtual = false;
}
if (_isVirtual) {
- virtualDisp = new VirtualMatrixPanel((*display), _rows, _cols, mxconfig.mx_width, mxconfig.mx_height, chainType);
- virtualDisp->setRotation(0);
- if (bc.type == TYPE_HUB75MATRIX_QS) {
- switch(panelHeight) {
+ virtualDisp = new(std::nothrow) VirtualMatrixPanel((*display), _rows, _cols, physicalPanelWidth, physicalPanelHeight, chainType);
+ if (!virtualDisp) { // catch alloc error
+ _isVirtual = false;
+ DEBUGBUS_PRINTLN(F("HUB75 virtual matrix: alloc failed, falling back to non-virtual driver"));
+ } else {
+ virtualDisp->setRotation(0);
+ if (bc.type == TYPE_HUB75MATRIX_QS) {
+ switch(panelHeight) {
case 16:
virtualDisp->setPhysicalPanelScanRate(FOUR_SCAN_16PX_HIGH);
break;
@@ -1025,6 +1086,7 @@ BusHub75Matrix::BusHub75Matrix(const BusConfig &bc) : Bus(bc.type, bc.start, bc.
DEBUGBUS_PRINTLN("Unsupported height");
cleanup();
return;
+ }
}
}
}
@@ -1079,7 +1141,7 @@ void IRAM_ATTR BusHub75Matrix::setPixelColor(unsigned pix, uint32_t c) {
uint32_t BusHub75Matrix::getPixelColor(unsigned pix) const {
if (!_valid) return IS_BLACK; // note: no need to check pix >= _len as that is checked in containsPixel()
if (_ledBuffer)
- return uint32_t(_ledBuffer[pix]);
+ return uint32_t(_ledBuffer[pix]); // fastled-slim already returns RGB, no need to mask out the upper byte
else
return getBitFromArray(_ledsDirty, pix) ? IS_DARKGREY: IS_BLACK; // just a hack - we only know if the pixel is black or not
}
@@ -1124,6 +1186,8 @@ void BusHub75Matrix::cleanup() {
if (display != nullptr) delete display;
display = nullptr;
virtualDisp = nullptr; // note: when not using "NO_GFX" this causes a memory leak
+ #else // runtime reconfiguration is not working on -S3, request reboot from user instead
+ errorFlag = ERR_REBOOT_NEEDED;
#endif
if (_ledBuffer != nullptr) d_free(_ledBuffer); _ledBuffer = nullptr;
if (_ledsDirty != nullptr) d_free(_ledsDirty); _ledsDirty = nullptr;
@@ -1144,8 +1208,8 @@ std::vector BusHub75Matrix::getLEDTypes() {
size_t BusHub75Matrix::getPins(uint8_t* pinArray) const {
if (pinArray) {
- pinArray[0] = mxconfig.mx_width;
- pinArray[1] = mxconfig.mx_height;
+ pinArray[0] = _isQuadScan ? mxconfig.mx_width /2 : mxconfig.mx_width;
+ pinArray[1] = _isQuadScan ? mxconfig.mx_height *2 : mxconfig.mx_height;
pinArray[2] = mxconfig.chain_length;
pinArray[3] = _rows;
pinArray[4] = _cols;
diff --git a/wled00/bus_manager.h b/wled00/bus_manager.h
index abfb08c81b..eeb10cff24 100644
--- a/wled00/bus_manager.h
+++ b/wled00/bus_manager.h
@@ -184,10 +184,9 @@ class Bus {
type == TYPE_NET_DDP_RGBW || type == TYPE_NET_ARTNET_RGBW; // network types with white channel
}
static constexpr bool hasCCT(uint8_t type) {
- return type == TYPE_WS2812_2CH_X3 || type == TYPE_WS2812_WWA ||
+ return type == TYPE_WS2812_WWA || type == TYPE_SM16825 ||
type == TYPE_ANALOG_2CH || type == TYPE_ANALOG_5CH ||
- type == TYPE_FW1906 || type == TYPE_WS2805 ||
- type == TYPE_SM16825;
+ type == TYPE_FW1906 || type == TYPE_WS2805;
}
static constexpr bool isTypeValid(uint8_t type) { return (type > 15 && type < 128); }
static constexpr bool isDigital(uint8_t type) { return (type >= TYPE_DIGITAL_MIN && type <= TYPE_DIGITAL_MAX) || is2Pin(type); }
@@ -442,6 +441,7 @@ class BusHub75Matrix : public Bus {
uint8_t _rows = 1; // panels per row
uint8_t _cols = 1; // panels per column
bool _isVirtual = false; // note: this is not strictly needed but there are padding bytes here anyway
+ bool _isQuadScan = false;
CRGB *_ledBuffer = nullptr; // note: using uint32_t buffer is only 2% faster and not worth the extra RAM
byte *_ledsDirty = nullptr;
// workaround for missing constants on include path for non-MM
diff --git a/wled00/bus_wrapper.h b/wled00/bus_wrapper.h
index 0ecd4f986d..15b5b0a3e1 100644
--- a/wled00/bus_wrapper.h
+++ b/wled00/bus_wrapper.h
@@ -1,4 +1,4 @@
-#pragma once
+#pragma once
#ifndef BusWrapper_h
#define BusWrapper_h
@@ -50,7 +50,7 @@
#define I_8266_U1_UCS_4 26
#define I_8266_DM_UCS_4 27
#define I_8266_BB_UCS_4 28
-//FW1906 GRBCW
+//FW1906 GRBCCT
#define I_8266_U0_FW6_5 29
#define I_8266_U1_FW6_5 30
#define I_8266_DM_FW6_5 31
@@ -60,7 +60,7 @@
#define I_8266_U1_APA106_3 34
#define I_8266_DM_APA106_3 35
#define I_8266_BB_APA106_3 36
-//WS2805 (RGBCW)
+//WS2805 (RGBCCT)
#define I_8266_U0_2805_5 37
#define I_8266_U1_2805_5 38
#define I_8266_DM_2805_5 39
@@ -70,7 +70,7 @@
#define I_8266_U1_TM1914_3 42
#define I_8266_DM_TM1914_3 43
#define I_8266_BB_TM1914_3 44
-//SM16825 (RGBCW)
+//SM16825 (RGBCCT)
#define I_8266_U0_SM16825_5 45
#define I_8266_U1_SM16825_5 46
#define I_8266_DM_SM16825_5 47
@@ -98,19 +98,19 @@
//UCS8904 (RGBW)
#define I_32_RN_UCS_4 25
#define I_32_I2_UCS_4 26
-//FW1906 GRBCW
+//FW1906 GRBCCT 6 color channels
#define I_32_RN_FW6_5 29
#define I_32_I2_FW6_5 30
//APA106
#define I_32_RN_APA106_3 33
#define I_32_I2_APA106_3 34
-//WS2805 (RGBCW)
+//WS2805 (RGBCCT)
#define I_32_RN_2805_5 37
#define I_32_I2_2805_5 38
//TM1914 (RGB)
#define I_32_RN_TM1914_3 41
#define I_32_I2_TM1914_3 42
-//SM16825 (RGBCW)
+//SM16825 (RGBCCT)
#define I_32_RN_SM16825_5 45
#define I_32_I2_SM16825_5 46
@@ -180,12 +180,12 @@
#define B_8266_U1_APA106_3 NeoPixelBus //3 chan, esp8266, gpio2
#define B_8266_DM_APA106_3 NeoPixelBus //3 chan, esp8266, gpio3
#define B_8266_BB_APA106_3 NeoPixelBus //3 chan, esp8266, bb (any pin but 16)
-//FW1906 GRBCW
+//FW1906 GRBCCT
#define B_8266_U0_FW6_5 NeoPixelBus //esp8266, gpio1
#define B_8266_U1_FW6_5 NeoPixelBus //esp8266, gpio2
#define B_8266_DM_FW6_5 NeoPixelBus //esp8266, gpio3
#define B_8266_BB_FW6_5 NeoPixelBus //esp8266, bb
-//WS2805 GRBCW
+//WS2805 GRBCCT
#define B_8266_U0_2805_5 NeoPixelBus //esp8266, gpio1
#define B_8266_U1_2805_5 NeoPixelBus //esp8266, gpio2
#define B_8266_DM_2805_5 NeoPixelBus //esp8266, gpio3
@@ -195,7 +195,7 @@
#define B_8266_U1_TM1914_3 NeoPixelBus
#define B_8266_DM_TM1914_3 NeoPixelBus
#define B_8266_BB_TM1914_3 NeoPixelBus
-//Sm16825 (RGBWC)
+//Sm16825 (RGBCCT)
#define B_8266_U0_SM16825_5 NeoPixelBus
#define B_8266_U1_SM16825_5 NeoPixelBus
#define B_8266_DM_SM16825_5 NeoPixelBus
@@ -285,11 +285,11 @@
#define B_32_RN_APA106_3 NeoPixelBus
#define B_32_I2_APA106_3 NeoPixelBus
#define B_32_IP_APA106_3 NeoPixelBus // parallel I2S
-//FW1906 GRBCW
+//FW1906 GRBCCT 6 color channels
#define B_32_RN_FW6_5 NeoPixelBus
#define B_32_I2_FW6_5 NeoPixelBus
#define B_32_IP_FW6_5 NeoPixelBus // parallel I2S
-//WS2805 RGBWC
+//WS2805 RGBCCT
#define B_32_RN_2805_5 NeoPixelBus
#define B_32_I2_2805_5 NeoPixelBus
#define B_32_IP_2805_5 NeoPixelBus // parallel I2S
@@ -297,7 +297,7 @@
#define B_32_RN_TM1914_3 NeoPixelBus
#define B_32_I2_TM1914_3 NeoPixelBus
#define B_32_IP_TM1914_3 NeoPixelBus // parallel I2S
-//Sm16825 (RGBWC)
+//Sm16825 (RGBCCT)
#define B_32_RN_SM16825_5 NeoPixelBus
#define B_32_I2_SM16825_5 NeoPixelBus
#define B_32_IP_SM16825_5 NeoPixelBus // parallel I2S
@@ -358,6 +358,7 @@ class PolyBus {
#ifdef ESP8266
dotStar_strip->Begin();
#else
+ if (miso == -1) miso = 127; // note: in arduino core, -1 means "default" not "none", passing 127 as the MISO pin is a workaround to prevent SPI.begin() assign the default pin, see #5670
if (sck == -1 && mosi == -1) dotStar_strip->Begin();
else dotStar_strip->Begin(sck, miso, mosi, ss);
#endif
@@ -1315,7 +1316,6 @@ class PolyBus {
if (offset > 3) offset = 3;
switch (busType) {
case TYPE_WS2812_1CH_X3:
- case TYPE_WS2812_2CH_X3:
case TYPE_WS2812_RGB:
case TYPE_WS2812_WWA:
t = I_8266_U0_NEO_3 + offset; break;
@@ -1358,7 +1358,6 @@ class PolyBus {
// Now determine actual bus type with the chosen offset
switch (busType) {
case TYPE_WS2812_1CH_X3:
- case TYPE_WS2812_2CH_X3:
case TYPE_WS2812_RGB:
case TYPE_WS2812_WWA:
t = I_32_RN_NEO_3 + offset; break;
diff --git a/wled00/colors.h b/wled00/colors.h
index 105048d35d..00fe4fb498 100644
--- a/wled00/colors.h
+++ b/wled00/colors.h
@@ -198,6 +198,11 @@ struct CRGBW {
uint8_t getAverageLight() const {
return (r + g + b + w) >> 2;
}
+
+ // get the average of the R, G, B values
+ uint8_t getRGBaverage() const {
+ return ((r + g + b) * 21846) >> 16; // x*21846>>16 is equal to "divide by 3"
+ }
};
inline CHSV32::CHSV32(const CRGBW& rgb) {
diff --git a/wled00/const.h b/wled00/const.h
index 70373316fd..04ff8ded61 100644
--- a/wled00/const.h
+++ b/wled00/const.h
@@ -319,7 +319,7 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit");
#define TYPE_DIGITAL_MIN 16 // first usable digital type
#define TYPE_WS2812_1CH 18 //white-only chips (1 channel per IC) (unused)
#define TYPE_WS2812_1CH_X3 19 //white-only chips (3 channels per IC)
-#define TYPE_WS2812_2CH_X3 20 //CCT chips (1st IC controls WW + CW of 1st zone and CW of 2nd zone, 2nd IC controls WW of 2nd zone and WW + CW of 3rd zone)
+//#define TYPE_WS2812_2CH_X3 20 // use FW1906
#define TYPE_WS2812_WWA 21 //amber + warm + cold white
#define TYPE_WS2812_RGB 22
#define TYPE_GS8608 23 //same driver as WS2812, but will require signal 2x per second (else displays test pattern)
@@ -472,6 +472,17 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit");
#define ERR_OVERTEMP 30 // An attached temperature sensor has measured above threshold temperature (not implemented)
#define ERR_OVERCURRENT 31 // An attached current sensor has measured a current above the threshold (not implemented)
#define ERR_UNDERVOLT 32 // An attached voltmeter has measured a voltage below the threshold (not implemented)
+#define ERR_LOW_MEM 33 // low memory (RAM)
+#define ERR_LOW_SEG_MEM 34 // low memory (effect data RAM)
+#define ERR_LOW_WS_MEM 35 // low memory (ws)
+//#define ERR_LOW_AJAX_MEM 36 // (not used any more) low memory (oappend)
+#define ERR_LOW_BUF 37 // low memory (LED pixels buffer)
+#define ERR_SYS_REBOOT 90 // reboot after error, trying to roll back
+#define ERR_SYS_BROWNOUT 91 // reboot after brownout alert
+#define ERR_PERSISTENT_THRESHOLD 100 // ToDO: errors below this value are non-persistent; persistent errors stay in the UI until restart
+// ERR_PERSISTENT_THRESHOLD is a threshold value only - never assign directly to errorFlag
+#define ERR_REBOOT_NEEDED 100 // reboot needed after changing hardware setting
+#define ERR_POWEROFF_NEEDED 101 // power-cycle needed after changing hardware setting
// JSON buffer lock owners
#define JSON_LOCK_UNKNOWN 255
diff --git a/wled00/data/common.js b/wled00/data/common.js
index e6cea4d526..d9df43400a 100644
--- a/wled00/data/common.js
+++ b/wled00/data/common.js
@@ -186,10 +186,10 @@ function connectWs(onOpen) {
// start: start pixel index
// len: number of pixels to send
// colors: Uint8Array with RGB values (3*len bytes)
-function sendDDP(ws, start, len, colors) {
+function sendDDP(ws, start, len, colors, isESP8266=false) {
if (!colors || colors.length < len * 3) return false; // not enough color data
- let maxDDPpx = 472; // must fit into one WebSocket frame of 1428 bytes, DDP header is 10+1 bytes -> 472 RGB pixels
- //let maxDDPpx = 172; // ESP8266: must fit into one WebSocket frame of 528 bytes -> 172 RGB pixels TODO: add support for ESP8266?
+ // data must fit into one WebSocket frame of 1428 bytes, DDP header is 10+1 bytes -> 472 RGB pixels (ESP8266: 528 bytes -> 172 RGB pixels)
+ let maxDDPpx = isESP8266 ? 172 : 472;
if (!ws || ws.readyState !== WebSocket.OPEN) return false;
// send in chunks of maxDDPpx
for (let i = 0; i < len; i += maxDDPpx) {
@@ -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
diff --git a/wled00/data/index.js b/wled00/data/index.js
index ee5126973c..c91b8b06a4 100644
--- a/wled00/data/index.js
+++ b/wled00/data/index.js
@@ -27,6 +27,7 @@ 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 _selFxInterval = null; // interval ID for selected effect position update
var cfg = {
theme:{base:"dark", bg:{url:"", rnd: false, rndGrayscale: false, rndBlur: false}, alpha:{bg:0.6,tab:0.8}, color:{bg:""}},
comp :{colors:{picker: true, rgb: false, quick: true, hex: false},
@@ -1567,8 +1568,36 @@ function readState(s,command=false)
case 19:
errstr = "A filesystem error has occured.";
break;
+// error code from WLEDMM - not supported yet
+// case 33:
+// errstr = "Low Memory (generic RAM).";
+// break;
+// case 34:
+// errstr = "Low Memory (effect data).";
+// break;
+// case 35:
+// errstr = "Low Memory (WS data).";
+// break;
+// case 36:
+// errstr = "Low Memory (oappend buffer).";
+// break;
+// case 37:
+// errstr = "no memory for LEDs buffer.";
+// break;
+ case 90:
+ errstr = "Unexpected Restart.";
+ break;
+ case 91:
+ errstr = "Brownout Restart.";
+ break;
+ case 100:
+ errstr = "Please reboot WLED to activate changed settings.";
+ break;
+ case 101:
+ errstr = "Please switch your device off and back on.";
+ break;
}
- showToast('Error ' + s.error + ": " + errstr, true);
+ showToast(((s.error<100) ? 'Error ': 'Note ') + s.error + ": " + errstr, true); // show "please restart" as a note, all others as errors
}
selectedPal = i.pal;
@@ -1645,7 +1674,8 @@ function setEffectParameters(idx)
}
setSelectedEffectPosition();
- setInterval(setSelectedEffectPosition,750);
+ if (_selFxInterval) clearInterval(_selFxInterval);
+ _selFxInterval = setInterval(setSelectedEffectPosition,750);
// set html color items on/off
var cslLabel = '';
var sep = '';
@@ -2818,14 +2848,7 @@ function rSegs()
cnfrS = false;
bt.style.color = "var(--c-f)";
bt.innerHTML = "Reset segments";
- var obj = {"seg":[{"start":0,"stop":ledCount,"sel":true}]};
- if (isM) {
- obj.seg[0].stop = mw;
- obj.seg[0].startX = 0;
- obj.seg[0].stopY = mh;
- }
- for (let i=1; i<=lSeg; i++) obj.seg.push({"stop":0});
- requestJson(obj);
+ requestJson({"rSeg": true}); // send reset segment request, calls makeAutoSegments() in firmware
}
function loadPalettesData() {
@@ -3597,4 +3620,4 @@ _C.addEventListener('touchstart', lock, false);
_C.addEventListener('mouseout', move, false);
_C.addEventListener('mouseup', move, false);
-_C.addEventListener('touchend', move, false);
\ No newline at end of file
+_C.addEventListener('touchend', move, false);
diff --git a/wled00/data/pixelforge/pixelforge.htm b/wled00/data/pixelforge/pixelforge.htm
index e8fb27b674..b828a0e927 100644
--- a/wled00/data/pixelforge/pixelforge.htm
+++ b/wled00/data/pixelforge/pixelforge.htm
@@ -252,6 +252,7 @@
+
Target Segment
@@ -320,6 +321,8 @@
Crop & Adjust Image
+
+
Not available on ESP8266
@@ -424,6 +427,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
+let is8266 = false; // restrictions apply for ESP8266, set when getting the info
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
@@ -434,15 +438,18 @@
Custom Fonts
const s = document.createElement('script');
s.src = 'common.js';
s.onerror = () => setTimeout(loadFiles, 100);
- s.onload = () => {
- loadResources(['style.css','omggif.js'], init); // load omggif.js then call init()
+ s.onload = async () => {
+ getLoc(); // set up loc/locip for getURL() before any fetch (file mode / reverse proxy)
+ await fsMem(); // update & show file system memory info, also updates wled version (wv) and is8266
+ const resources = ['style.css'];
+ if (!is8266) resources.push('omggif.js'); // omggif is not available on ESP8266
+ loadResources(resources, init); // load omggif.js then call init()
};
document.head.appendChild(s);
})();
/* init */
async function init() {
- getLoc();
// create off screen canvas
rv = cE('canvas');
rvc = rv.getContext('2d',{willReadFrequently:true});
@@ -450,7 +457,6 @@
Custom Fonts
await flU(); // update file list
tabSw(localStorage.tab||'img'); // switch to last open tab or image tab by default
await segLoad(); // load available segments
- await fsMem(); // update & show file system memory info, also updates wled version (wv)
await loadTools(); // load additional tools list from pftools.json
}
@@ -707,6 +713,7 @@
${esc(t.name)} v${esc(t.ver)}
const m = info.ver.match(/\d+/g); // extract all numbers from version string (e.g. "16.1.0-beta" → [16, 1])
wv = [parseInt(m[0]) || 0, parseInt(m[1]) || 0];
}
+ if (info.arch === 'esp8266') is8266 = true;
}
}catch(e){console.error(e);}
}
@@ -1263,7 +1270,15 @@
${esc(t.name)} v${esc(t.ver)}
getId(id).classList.toggle('active', tab===['img','txt','oth'][i%3]);
});
localStorage.tab=tab;
- ({txt:()=>{txtSegLoad(); scanFonts();}, img:imgLoad}[tab]||(()=>{}))(); // on tab switch, load images and available fonts
+ if (tab === 'img') {
+ getId('iTab8266').style.display = is8266 ? 'block' : 'none'; // show "not available" on ESP8266
+ getId('iTabContent').style.display = is8266 ? 'none' : ''; // show normal image tool on ESP32
+ if (!is8266) imgLoad();
+ }
+ if (tab === 'txt') {
+ txtSegLoad();
+ scanFonts();
+ }
}
'Img,Txt,Oth'.split(',').forEach((s,i)=>{
getId('t'+s).onclick=()=>tabSw(['img','txt','oth'][i]);
diff --git a/wled00/data/settings_leds.htm b/wled00/data/settings_leds.htm
index f1471e9bf4..03fae259c4 100644
--- a/wled00/data/settings_leds.htm
+++ b/wled00/data/settings_leds.htm
@@ -158,10 +158,9 @@
}
}
};
- if (bquot > 100) {var msg = "Too many LEDs! Can't handle that!"; alert(msg); e.stopPropagation(); return false;}
+ if (bquot > 100) {alert("Too many LEDs! Can't handle that!"); e.stopPropagation(); return false;}
else {
- if (bquot > 80) {var msg = "Memory usage is high, reboot recommended!\n\rSet transitions to 0 to save memory.";
- if (bquot > 100) msg += "\n\rToo many LEDs for me to handle properly!"; if (maxM < 10000) msg += "\n\rConsider using an ESP32."; alert(msg);}
+ if (bquot > 80) alert("Memory usage is high, reboot recommended!\n\rSet transitions to 0 to save memory.");
if (!d.Sf.ABL.checked || d.Sf.PPL.checked) d.Sf.MA.value = 0; // submit 0 as ABL (PPL will handle it)
if (d.Sf.checkValidity()) {
d.Sf.querySelectorAll("#mLC select[name^=LT]").forEach((s)=>{s.disabled=false;}); // just in case
@@ -382,15 +381,11 @@
d.Sf.CR.checked = false;
}
// update start indexes, max values, calculate current, etc
- let sameType = 0;
var nList = d.Sf.querySelectorAll("#mLC input[name^=L]");
nList.forEach((LC,i)=>{
let nm = LC.name.substring(0,2); // field name : /L./
let n = LC.name.substring(2,3); // bus number (0-Z)
let t = parseInt(d.Sf["LT"+n].value); // LED type SELECT
- if (isDig(t) && !isD2P(t)) {
- if (sameType == 0) sameType = t; // first bus type
- }
// do we have a led count field
if (nm=="LC") {
if (!isHub75(t)) {
@@ -470,8 +465,8 @@
}
}
});
+ // note: do not remove this second call to updateTypeDropdowns() as it also updates the available LED types based on the current bus configuration, not just the driver options
updateTypeDropdowns(); // update type dropdowns to disable unavailable digital/analog types (I2S/RMT bus count may have changed due to memory usage change)
- // note: do not remvoe this second call to updateTypeDropdowns() as it also updates the available LED types based on the current bus configuration, not just the driver options
// Show channel usage warning
let chanuse = gId('chanuse');
@@ -770,7 +765,7 @@
if (c.hw) {
if (c.hw.led) {
// remove all existing outputs
- for (const i=0; i<36; i++) addLEDs(-1); // was i{
addLEDs(1);
@@ -1126,8 +1121,7 @@
General settings
Power up
Turn LEDs on after power up/reset:
- with brightness: (1-255)
- (disable if using boot preset to turn LEDs on)
+ Bootup brightness: (1-255)
Apply preset at boot (0 = none)
diff --git a/wled00/data/settings_time.htm b/wled00/data/settings_time.htm
index ab2c4a7fc0..cc98fd1b1b 100644
--- a/wled00/data/settings_time.htm
+++ b/wled00/data/settings_time.htm
@@ -190,7 +190,7 @@
}
}
function pMP() { // populateMacroPresets
- var presetOpts = '' + sortedPresetOptions;
+ var presetOpts = '' + sortedPresetOptions;
var fields = ['A0','A1','MC','MN'];
for (var f of fields) {
var inp = gN(f);
@@ -219,13 +219,38 @@
rPS(sel, presetOpts, "data-preset");
}
}
+ function bAO() { // buildAnalogOptions: analog functions + per-segment opacity (segment 0 included; MD=0 => segment 0)
+ var o = '';
+ return o;
+ }
+ function isAnalogBtn(t) { return t==7 || t==8; } // BTN_TYPE_ANALOG / BTN_TYPE_ANALOG_INVERTED
+ function isSwitchBtn(t) { return t==4 || t==5 || t==9; } // BTN_TYPE_SWITCH / BTN_TYPE_PIR_SENSOR / BTN_TYPE_TOUCH_SWITCH
+ function btnTypeName(t) { // mirrors the button type dropdown on the LED settings page
+ switch (+t) {
+ case 2: return 'Pushbutton';
+ case 3: return 'Push inverted';
+ case 4: return 'Switch';
+ case 5: return 'PIR sensor';
+ case 6: return 'Touch';
+ case 7: return 'Analog';
+ case 8: return 'Analog inverted';
+ case 9: return 'Touch (switch)';
+ default: return 'Disabled';
+ }
+ }
function rBPO() { // refreshButtonPresetOptions
- var presetOpts = '' + sortedPresetOptions;
+ var presetOpts = '' + sortedPresetOptions;
+ var analogOpts = bAO();
var container = gId("macros");
if (!container) return;
+ // analog buttons only have an MD select (MP/ML are hidden 0 inputs); MD uses analog options, never presets
var sels = container.querySelectorAll('select[name^="MP"],select[name^="ML"],select[name^="MD"]');
for (var sel of sels) {
- rPS(sel, presetOpts, "data-preset");
+ var bb = sel.closest ? sel.closest(".bb") : null;
+ var t = bb ? parseInt(bb.getAttribute("data-btype")||"0",10) : 0;
+ rPS(sel, isAnalogBtn(t) ? analogOpts : presetOpts, "data-preset");
}
}
function Wd()
@@ -246,33 +271,73 @@
if (d.Sf.LTR.value==="S") { d.Sf.LT.value = -1*parseFloat(d.Sf.LT.value); }
if (d.Sf.LNR.value==="W") { d.Sf.LN.value = -1*parseFloat(d.Sf.LN.value); }
}
- function addRow(i,p,l,d) {
+ function addRow(i,p,l,d,t) {
+ if (t===undefined) t = 0;
var b = String.fromCharCode((i<10?48:55)+i);
- var presetOpts = '' + sortedPresetOptions;
+ var presetOpts = '' + sortedPresetOptions;
+ var typeName = btnTypeName(t);
var buttonBlock = document.createElement('div');
buttonBlock.className = 'bb';
- buttonBlock.innerHTML = `
-
Button ${i}
-
-
-
-
+ buttonBlock.setAttribute('data-btype', t); // read back by rBPO() to rebuild selects correctly
+ if (isAnalogBtn(t)) {
+ // analog buttons: MD holds the function/segment; short/long press are unused (firmware defaults missing MP/ML to 0)
+ buttonBlock.innerHTML = `
+
Analog ${i} - ${typeName}
+
+
+
+
+
-
-
-
+
+ `;
+ sPSV(buttonBlock.querySelector('select[name="MD'+b+'"]'), String(d), "data-preset");
+ } else if (isSwitchBtn(t)) {
+ // switches: MP fires on On->Off, ML on Off->On; double press (MD) is unused (firmware defaults missing MD to 0)
+ buttonBlock.innerHTML = `
+
Switch ${i} - ${typeName}
+
+
+
+
+
+
+
+
+
-
-
-
+
+ `;
+ var switchSels = buttonBlock.querySelectorAll("select");
+ var switchVals = [String(p), String(l)];
+ for (var si=0; siButton ${i} - ${typeName}
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
- `;
- var buttonSels = buttonBlock.querySelectorAll("select");
- var buttonVals = [String(p), String(l), String(d)];
- for (var si=0; si
+ `;
+ var buttonSels = buttonBlock.querySelectorAll("select");
+ var buttonVals = [String(p), String(l), String(d)];
+ for (var si=0; si{
if (d.um_p[0]==-1) d.um_p.shift(); // remove filler
d.Sf.SDA.max = d.Sf.SCL.max = d.Sf.MOSI.max = d.Sf.SCLK.max = d.Sf.MISO.max = d.max_gpio;
@@ -160,6 +161,7 @@
}
function UI(e) {
// update changed select options across all usermods
+ if (!e.classList.contains("pin")) return; // not a pin select, ignore. fix for #5759
let oldV = parseInt(e.dataset.val);
e.dataset.val = e.value;
let txt = e.name.split(":")[e.name.split(":").length-2];
@@ -194,7 +196,11 @@
um += ":"+fld;
} else if (typeof(fld) === "number") sel.classList.add("pin"); // a hack to add a class
let arr = d.getElementsByName(um);
- let idx = arr[0].type==="hidden"?1:0; // ignore hidden field
+ if (!arr || arr.length === 0) {
+ console.log("addDD: No elements found for name:", um);
+ return null; // no elements found
+ }
+ let idx = (arr[0] && arr[0].type==="hidden")?1:0; // ignore hidden field
if (arr.length > 1+idx) {
// we have array of values (usually pins)
for (let i of arr) {
@@ -256,6 +262,98 @@
e.preventDefault();
if (d.Sf.checkValidity()) d.Sf.submit(); //https://stackoverflow.com/q/37323914
}
+
+ // TODO: rename this function, needs to be in sync with the now out of tree mod
+ function aOpt(name,el) {
+ let obj = d.getElementsByName(name);
+ if (!obj || obj.length === 0) return; // No elements found
+
+ var select = obj;
+ if (obj[el]) select = obj[el];
+
+ // Check if it's actually a select element with options
+ if (!select.options || !select.options.length) return;
+
+ for (let i=0; i= valFrom && c.value <= valTo) {
+ select.removeChild(c);
+ i--; //decrease i by one because the index has been adjusted
+ }
+ //https://www.javascripttutorial.net/javascript-dom/javascript-add-remove-options/
+ //https://www.javascripttutorial.net/javascript-dom/javascript-remove-items-from-a-select-conditionally/
+ }
+ }
+ function dRO(name,el) {
+ // Initialize d.ro_gpio if not already set
+ if (!d.ro_gpio) d.ro_gpio = [];
+
+ let obj = d.getElementsByName(name);
+ if (!obj || obj.length === 0) return; // No elements found
+
+ var select = obj;
+ if (obj[el]) select = obj[el];
+
+ // Check if it's actually a select element with options
+ if (!select.options || !select.options.length) return;
+
+ // console.log("dRO", name, el, obj, "s", select, d.ro_gpio);
+ for (let i=0; i
diff --git a/wled00/data/settings_wifi.htm b/wled00/data/settings_wifi.htm
index e187f887fb..d06d501087 100644
--- a/wled00/data/settings_wifi.htm
+++ b/wled00/data/settings_wifi.htm
@@ -65,10 +65,6 @@
select.setAttribute("onchange", "T(this)");
preScanSSID = input.value;
- for (let i = 0; i < select.children.length; i++) {
- select.removeChild(select.children[i]);
- }
-
for (let i = 0; i < networks.length; i++) {
const option = cE("option");
diff --git a/wled00/data/update.htm b/wled00/data/update.htm
index 89238e7abb..3465d6b74c 100644
--- a/wled00/data/update.htm
+++ b/wled00/data/update.htm
@@ -151,9 +151,9 @@