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://img.shields.io/badge/send%20me%20a%20small%20gift-paypal-blue.svg?style=flat-square)](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

+ + @@ -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 = ''; + for (var j=0; j<=32; j++) o += ``; + 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 @@

Manual upload



- +
-
+
diff --git a/wled00/dmx_input.cpp b/wled00/dmx_input.cpp index 8a1aedef00..c044f046c2 100644 --- a/wled00/dmx_input.cpp +++ b/wled00/dmx_input.cpp @@ -151,9 +151,9 @@ void DMXInput::init(int8_t rxPin, int8_t txPin, int8_t enPin, uint8_t inputPortN const bool pinsAllocated = PinManager::allocateMultiplePins(pins, 3, PinOwner::DMX_INPUT); if (!pinsAllocated) { DEBUG_PRINTF("DMXInput: Error: Failed to allocate pins for DMX_INPUT. Pins already in use:\n"); - DEBUG_PRINTF("rx in use by: %s\n", PinManager::getPinOwner(rxPin)); - DEBUG_PRINTF("tx in use by: %s\n", PinManager::getPinOwner(txPin)); - DEBUG_PRINTF("en in use by: %s\n", PinManager::getPinOwner(enPin)); + DEBUG_PRINTF("rx in use by: %u (%s)\n", unsigned(PinManager::getPinOwner(rxPin)), PinManager::getPinOwnerName(rxPin)); + DEBUG_PRINTF("tx in use by: %u (%s)\n", unsigned(PinManager::getPinOwner(txPin)), PinManager::getPinOwnerName(txPin)); + DEBUG_PRINTF("en in use by: %u (%s)\n", unsigned(PinManager::getPinOwner(enPin)), PinManager::getPinOwnerName(enPin)); return; } diff --git a/wled00/e131.cpp b/wled00/e131.cpp index 6846e5124e..727800184d 100644 --- a/wled00/e131.cpp +++ b/wled00/e131.cpp @@ -5,7 +5,7 @@ #define MAX_CHANNELS_PER_UNIVERSE 512 // forward declarations -static void handleDDPPacket(e131_packet_t* p); +static void handleDDPPacket(e131_packet_t* p, size_t packetLen); static void handleArtnetPollReply(IPAddress ipAddress); static void prepareArtnetPollReply(ArtPollReply *reply); static void sendArtnetPollReply(ArtPollReply *reply, IPAddress ipAddress, uint16_t portAddress); @@ -17,20 +17,31 @@ static void sendArtnetPollReply(ArtPollReply *reply, IPAddress ipAddress, uint16 //DDP protocol support, called by handleE131Packet //handles RGB data only -static void handleDDPPacket(e131_packet_t* p) { +static void handleDDPPacket(e131_packet_t* p, size_t packetLen) { static bool ddpSeenPush = false; // have we seen a push yet? int lastPushSeq = e131LastSequenceNumber[0]; + if (packetLen < DDP_HEADER_LEN) return; // too short to safely read any DDP header fields + // reject unsupported color data types (only RGB and RGBW are supported) - uint8_t maskedType = p->dataType & 0x3F; // mask out custom and reserved flags, only type bits are relevant - if (maskedType != DDP_TYPE_RGB24 && maskedType != DDP_TYPE_RGBW32) return; + //uint8_t maskedType = p->dataType & 0x3F; // mask out custom and reserved flags, only type bits are relevant + //if (maskedType != DDP_TYPE_RGB24 && maskedType != DDP_TYPE_RGBW32) return; + + // note: for maximum compatibility we do not reject unknonw or malformed data types but simply default to RGB24 and check there is enough data available in the packet to do so + // also we assume 8bit per channel and currently do not support other bit depths + + // reject control, status and config packets (not implemented) + if (p->destination == DDP_ID_CONTROL || p->destination == DDP_ID_STATUS || p->destination == DDP_ID_CONFIG) return; + + // reject query and response packets (not implemented) + if (p->flags & (DDP_FLAGS_QUERY | DDP_FLAGS_REPLY)) return; - // reject status and config packets (not implemented) - if (p->destination == DDP_ID_STATUS || p->destination == DDP_ID_CONFIG) return; + bool push = p->flags & DDP_FLAGS_PUSH; // push flag means "render now" + if (!push && (p->flags & DDP_FLAGS_STORAGE)) return; // reject "from storage" flag but still let the push flag pass if set along with it - //reject late packets belonging to previous frame (assuming 4 packets max. before push) + //reject late packets belonging to previous frame (assuming 4 packets max. before push, if more are used and packets are very late, they are still accepted) if (e131SkipOutOfSequence && lastPushSeq) { - int sn = p->sequenceNum & 0xF; + int sn = p->sequenceNum & 0xF; // sequence number is 4 bits, 1-15, 0 means unused if (sn) { if (lastPushSeq > 5) { if (sn > (lastPushSeq -5) && sn < lastPushSeq) return; @@ -40,7 +51,8 @@ static void handleDDPPacket(e131_packet_t* p) { } } - unsigned ddpChannelsPerLed = ((p->dataType & 0b00111000)>>3 == 0b011) ? 4 : 3; // data type 0x1B (formerly 0x1A) is RGBW (type 3, 8 bit/channel) + unsigned ddpChannelsPerLed = 3; // default to RGB + if ((p->dataType & 0b00111000)>>3 == 0b011) ddpChannelsPerLed = 4; // RGBW data type (see DDP protocol definition) uint32_t start = htonl(p->channelOffset) / ddpChannelsPerLed; start += DMXAddress / ddpChannelsPerLed; @@ -50,8 +62,14 @@ static void handleDDPPacket(e131_packet_t* p) { unsigned c = 0; if (p->flags & DDP_FLAGS_TIME) c = 4; //packet has timecode flag, we do not support it, but data starts 4 bytes later + // ensure the received packet is at least as long as the header claims + if (packetLen < DDP_HEADER_LEN + c + dataLen) { + DEBUG_PRINTLN(F("DDP packet incomplete")); + return; + } + unsigned numLeds = stop - start; // stop >= start is guaranteed - unsigned maxDataIndex = c + numLeds * ddpChannelsPerLed; // validate bounds before accessing data array + unsigned maxDataIndex = numLeds * ddpChannelsPerLed; // validate bounds before accessing data array if (maxDataIndex > dataLen) { DEBUG_PRINTLN(F("DDP packet data bounds exceeded, rejecting.")); return; @@ -66,7 +84,6 @@ static void handleDDPPacket(e131_packet_t* p) { } } - bool push = p->flags & DDP_FLAGS_PUSH; ddpSeenPush |= push; if (!ddpSeenPush || push) { // if we've never seen a push, or this is one, render display e131NewData = true; @@ -76,7 +93,7 @@ static void handleDDPPacket(e131_packet_t* p) { } //E1.31 and Art-Net protocol support -void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol){ +void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol, size_t packetLen){ int uni = 0, dmxChannels = 0; uint8_t* e131_data = nullptr; @@ -84,6 +101,7 @@ void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol){ if (protocol == P_ARTNET) { + if (packetLen < 10) return; // need at least art_opcode (offset 8, 2 bytes) if (p->art_opcode == ARTNET_OPCODE_OPPOLL) { handleArtnetPollReply(clientIP); return; @@ -96,9 +114,9 @@ void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol){ } else if (protocol == P_E131) { // Ignore PREVIEW data (E1.31: 6.2.6) if ((p->options & 0x80) != 0) return; - dmxChannels = htons(p->property_value_count) - 1; + dmxChannels = htons(p->property_value_count) - 1; // on malformed packets, this can become negative, checked below // DMX level data is zero start code. Ignore everything else. (E1.11: 8.5) - if (dmxChannels == 0 || p->property_values[0] != 0) return; + if (dmxChannels <= 0 || p->property_values[0] != 0) return; uni = htons(p->universe); e131_data = p->property_values; seq = p->sequence_number; @@ -110,7 +128,7 @@ void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol){ } } else { //DDP realtimeIP = clientIP; - handleDDPPacket(p); + handleDDPPacket(p, packetLen); return; } diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index ffb2c1202f..6201a19192 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -100,7 +100,7 @@ void initDMXInput(); void handleDMXInput(); //e131.cpp -void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol); +void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol, size_t packetLen); void handleDMXData(uint16_t uni, uint16_t dmxChannels, uint8_t* e131_data, uint8_t mde, uint8_t previousUniverses); // void handleArtnetPollReply(IPAddress ipAddress); // local function, only used in e131.cpp // void prepareArtnetPollReply(ArtPollReply* reply); // local function, only used in e131.cpp @@ -524,10 +524,10 @@ inline size_t getFreeHeapSize() { return ESP.getFreeHeap(); } // returns free he inline size_t getContiguousFreeHeap() { return ESP.getMaxFreeBlockSize(); } // returns largest contiguous free block #endif #define BFRALLOC_NOBYTEACCESS (1 << 0) // ESP32 has 32bit accessible DRAM (usually ~50kB free) that must not be byte-accessed -#define BFRALLOC_PREFER_DRAM (1 << 1) // prefer DRAM over PSRAM -#define BFRALLOC_ENFORCE_DRAM (1 << 2) // use DRAM only, no PSRAM -#define BFRALLOC_PREFER_PSRAM (1 << 3) // prefer PSRAM over DRAM -#define BFRALLOC_ENFORCE_PSRAM (1 << 4) // use PSRAM if available, otherwise uses DRAM +#define BFRALLOC_PREFER_DRAM (1 << 1) // prefer DRAM over PSRAM (can still use PSRAM for larger allocations if DRAM is starting to run low) +#define BFRALLOC_ENFORCE_DRAM (1 << 2) // use DRAM only, no PSRAM allowed +#define BFRALLOC_PREFER_PSRAM (1 << 3) // prefer PSRAM over DRAM (can still use DRAM if there is loads of free DRAM to optimize speed) +#define BFRALLOC_ENFORCE_PSRAM (1 << 4) // use PSRAM if available, falls back to DRAM if PSRAM fails #define BFRALLOC_CLEAR (1 << 5) // clear allocated buffer after allocation void *allocate_buffer(size_t size, uint32_t type); diff --git a/wled00/image_loader.cpp b/wled00/image_loader.cpp index 7757553928..d74afc68d1 100644 --- a/wled00/image_loader.cpp +++ b/wled00/image_loader.cpp @@ -30,7 +30,7 @@ int fileReadCallback(void) { int fileReadBlockCallback(void * buffer, int numberOfBytes) { #ifdef CONFIG_IDF_TARGET_ESP32C3 unsigned t0 = millis(); - while (strip.isUpdating() && (millis() - t0 < 15)) yield(); // be nice, but not too nice. Waits up to 15ms to avoid glitches + while (strip.isUpdating() && (millis() - t0 < 150)) yield(); // be nice, but not too nice. Waits up to 150ms to avoid glitches #endif return file.read((uint8_t*)buffer, numberOfBytes); } diff --git a/wled00/improv.cpp b/wled00/improv.cpp index 2d2d876d6e..d895ee6187 100644 --- a/wled00/improv.cpp +++ b/wled00/improv.cpp @@ -116,8 +116,8 @@ void handleImprovPacket() { return; } } else if (packetByte > 9) { //RPC data - rpcData[packetByte - 10] = next; if (packetByte > 137) return; //prevent buffer overflow + rpcData[packetByte - 10] = next; } } } @@ -208,7 +208,7 @@ void sendImprovInfoResponse() { #endif //Use serverDescription if it has been changed from the default "WLED", else mDNS name bool useMdnsName = (strcmp(serverDescription, "WLED") == 0 && strlen(cmDNS) > 0); - char vString[32]; + char vString[WLED_VERSION_MAX_LEN + 12] = {'\0'}; sprintf_P(vString, PSTR("%s/%i"), versionString, VERSION); const char *str[4] = {"WLED", vString, bString, useMdnsName ? cmDNS : serverDescription}; @@ -238,8 +238,8 @@ void handleImprovWifiScan() { bool isOpen = WiFi.encryptionType(i) == WIFI_AUTH_OPEN; #endif - char ssidStr[33]; - strcpy(ssidStr, WiFi.SSID(i).c_str()); + char ssidStr[33] = {'\0'}; + strlcpy(ssidStr, WiFi.SSID(i).c_str(), sizeof(ssidStr)); const char *str[3] = {ssidStr, rssiStr, isOpen ? "NO":"YES"}; sendImprovRPCResult(ImprovRPCType::Request_Scan, 3, str); } @@ -258,14 +258,13 @@ static void parseWiFiCommand(char* rpcData) { unsigned ssidLen = rpcData[1]; if (ssidLen > len -1 || ssidLen > 32) return; - memset(multiWiFi[0].clientSSID, 0, 32); + memset(multiWiFi[0].clientSSID, 0, sizeof(multiWiFi[0].clientSSID)); memcpy(multiWiFi[0].clientSSID, rpcData+2, ssidLen); - memset(multiWiFi[0].clientPass, 0, 64); + memset(multiWiFi[0].clientPass, 0, sizeof(multiWiFi[0].clientPass)); if (len > ssidLen +1) { unsigned passLen = rpcData[2+ssidLen]; - memset(multiWiFi[0].clientPass, 0, 64); - memcpy(multiWiFi[0].clientPass, rpcData+3+ssidLen, passLen); + memcpy(multiWiFi[0].clientPass, rpcData+3+ssidLen, min(size_t(passLen), sizeof(multiWiFi[0].clientPass))); } sendImprovStateResponse(0x03); //provisioning diff --git a/wled00/json.cpp b/wled00/json.cpp index 74e12b5470..19c40a3811 100644 --- a/wled00/json.cpp +++ b/wled00/json.cpp @@ -85,6 +85,9 @@ static bool deserializeSegment(JsonObject elem, byte it, byte presetId = 0) //DEBUG_PRINTLN(F("-- JSON deserialize segment.")); Segment& seg = strip.getSegment(id); + if (newSeg && presetId == 0) { + seg.colors[0] = DEFAULT_COLOR; // set color of newly created segment to warm orange as an indicator to the user + } // we do not want to make segment copy as it may use a lot of RAM (effect data and pixel buffer) // so we will create a copy of segment options and compare it with original segment when done processing SegmentCopy prev = { @@ -485,6 +488,14 @@ bool deserializeState(JsonObject root, byte callMode, byte presetId) } strip.resume(); } + // reset segment request + if (root[F("rSeg")] | false) { + strip.suspend(); + strip.waitForIt(); + strip.makeAutoSegments(true); // respects autoSegments flag + strip.resume(); + stateChanged = true; + } UsermodManager::readFromJsonState(root); diff --git a/wled00/led.cpp b/wled00/led.cpp index 35f5003679..131ff95bab 100644 --- a/wled00/led.cpp +++ b/wled00/led.cpp @@ -126,7 +126,6 @@ void stateUpdated(byte callMode) { jsonTransitionOnce = false; transitionActive = false; applyFinalBri(); - strip.trigger(); } else { if (transitionActive) { briOld = briT; @@ -232,7 +231,10 @@ void handleNightlight() { { for (unsigned i=0; i<4; i++) colPri[i] = colNlT[i]+ ((colSec[i] - colNlT[i])*nper); // fading from actual color to secondary color } + uint16_t transitionduration = strip.getTransition(); + strip.setTransition(0); // temporary disable transition and set color & brightness directly, (hacky fix for #5620) colorUpdated(CALL_MODE_NO_NOTIFY); + strip.setTransition(transitionduration); // restore transition time to previous value. Note: this needs proper fixing by disabling transitions completely in nightlight mode, reference implementation https://github.com/blazoncek/WLED/commit/c01a6b774969b652c30e383073958302042fd1f9 } if (nper >= 1) //nightlight duration over { diff --git a/wled00/network.cpp b/wled00/network.cpp index e97bf70abd..83e196178d 100644 --- a/wled00/network.cpp +++ b/wled00/network.cpp @@ -87,7 +87,7 @@ const ethernet_settings ethernetBoards[] = { // ESP32-ETHERNET-KIT-VE { - 0, // eth_address, + 1, // eth_address, 5, // eth_power, 23, // eth_mdc, 18, // eth_mdio, @@ -177,6 +177,12 @@ const ethernet_settings ethernetBoards[] = { }, }; +// sanity checks for ethernet config table and WLED_ETH_DEFAULT +static_assert((sizeof(ethernetBoards)/sizeof(ethernetBoards[0])) == WLED_NUM_ETH_TYPES, "WLED_NUM_ETH_TYPES does not match size of ethernetBoards[] table."); +#ifdef WLED_ETH_DEFAULT + static_assert(((WLED_ETH_DEFAULT) >= WLED_ETH_NONE) && ((WLED_ETH_DEFAULT) < WLED_NUM_ETH_TYPES), "WLED_ETH_DEFAULT is out of range."); +#endif + bool initEthernet() { static bool successfullyConfiguredEthernet = false; @@ -415,7 +421,8 @@ void WiFiEvent(WiFiEvent_t event) switch (event) { case ARDUINO_EVENT_WIFI_AP_STADISCONNECTED: // AP client disconnected - if (--apClients == 0 && isWiFiConfigured()) forceReconnect = true; // no clients reconnect WiFi if awailable + if (apClients > 0) apClients--; + if (apClients == 0 && isWiFiConfigured()) forceReconnect = true; // no clients reconnect WiFi if available DEBUG_PRINTF_P(PSTR("WiFi-E: AP Client Disconnected (%d) @ %lus.\n"), (int)apClients, millis()/1000); break; case ARDUINO_EVENT_WIFI_AP_STACONNECTED: @@ -442,10 +449,26 @@ void WiFiEvent(WiFiEvent_t event) } break; #ifdef ARDUINO_ARCH_ESP32 + case ARDUINO_EVENT_WIFI_READY: + DEBUG_PRINTLN(F("WiFi-E: driver ready.")); + break; case ARDUINO_EVENT_WIFI_SCAN_DONE: // also triggered when connected to selected SSID DEBUG_PRINTLN(F("WiFi-E: SSID scan completed.")); break; + case ARDUINO_EVENT_WIFI_STA_START: + DEBUG_PRINTLN(F("WiFi-E: STA Started")); + break; + case ARDUINO_EVENT_WIFI_STA_STOP: + DEBUG_PRINTLN(F("WiFi-E: STA Stopped")); + break; + case ARDUINO_EVENT_WIFI_STA_AUTHMODE_CHANGE: + DEBUG_PRINTLN(F("WiFi-E: STA authentication mode change.")); + break; + case ARDUINO_EVENT_WIFI_STA_LOST_IP: + DEBUG_PRINTLN(F("WiFi-E: IP address lost.")); + break; + case ARDUINO_EVENT_WIFI_AP_START: DEBUG_PRINTLN(F("WiFi-E: AP Started")); break; diff --git a/wled00/src/dependencies/e131/ESPAsyncE131.cpp b/wled00/src/dependencies/e131/ESPAsyncE131.cpp index 75d6b8dc29..8961e75e8a 100644 --- a/wled00/src/dependencies/e131/ESPAsyncE131.cpp +++ b/wled00/src/dependencies/e131/ESPAsyncE131.cpp @@ -21,10 +21,10 @@ #include "../network/Network.h" #include -// E1.17 ACN Packet Identifier +// E1.17 ACN Packet Identifier "ASC-E1.17" const byte ESPAsyncE131::ACN_ID[12] = { 0x41, 0x53, 0x43, 0x2d, 0x45, 0x31, 0x2e, 0x31, 0x37, 0x00, 0x00, 0x00 }; -// Art-Net Packet Identifier +// Art-Net Packet Identifier "Art-Net" const byte ESPAsyncE131::ART_ID[8] = { 0x41, 0x72, 0x74, 0x2d, 0x4e, 0x65, 0x74, 0x00 }; // Constructor @@ -99,36 +99,43 @@ bool ESPAsyncE131::initMulticast(uint16_t port, uint16_t universe, uint8_t n) { void ESPAsyncE131::parsePacket(AsyncUDPPacket _packet) { bool error = false; - uint8_t protocol = P_E131; + uint8_t protocol = P_ARTNET; + const size_t pktLen = _packet.length(); e131_packet_t *sbuff = reinterpret_cast(_packet.data()); - - //E1.31 packet identifier ("ACS-E1.17") - if (memcmp(sbuff->acn_id, ESPAsyncE131::ACN_ID, sizeof(sbuff->acn_id))) - protocol = P_ARTNET; - + + // E1.31 packet identifier (ACN_ID = "ASC-E1.17"), need at least 16 bytes to safely read acn_id (offset 4, length 12). + if (pktLen >= 16) { + if (!memcmp(sbuff->acn_id, ESPAsyncE131::ACN_ID, sizeof(sbuff->acn_id))) + protocol = P_E131; + } + if (protocol == P_ARTNET) { - if (memcmp(sbuff->art_id, ESPAsyncE131::ART_ID, sizeof(sbuff->art_id))) - error = true; //not "Art-Net" - if (sbuff->art_opcode != ARTNET_OPCODE_OPDMX && sbuff->art_opcode != ARTNET_OPCODE_OPPOLL) - error = true; //not a DMX or poll packet - } else { //E1.31 error handling - if (htonl(sbuff->root_vector) != ESPAsyncE131::VECTOR_ROOT) - error = true; - if (htonl(sbuff->frame_vector) != ESPAsyncE131::VECTOR_FRAME) - error = true; - if (sbuff->dmp_vector != ESPAsyncE131::VECTOR_DMP) - error = true; - if (sbuff->property_values[0] != 0) - error = true; - } - + if (pktLen < 10) { + error = true; // Need at least Art-Net ID (8) + opcode (2) + } else { + if (memcmp(sbuff->art_id, ESPAsyncE131::ART_ID, sizeof(sbuff->art_id))) + error = true; //not "Art-Net" + if (sbuff->art_opcode != ARTNET_OPCODE_OPDMX && sbuff->art_opcode != ARTNET_OPCODE_OPPOLL) + error = true; //not a DMX or poll packet + } + } else { //E1.31 error handling + if (htonl(sbuff->root_vector) != ESPAsyncE131::VECTOR_ROOT) + error = true; + if (htonl(sbuff->frame_vector) != ESPAsyncE131::VECTOR_FRAME) + error = true; + if (sbuff->dmp_vector != ESPAsyncE131::VECTOR_DMP) + error = true; + if (sbuff->property_values[0] != 0) + error = true; + } + if (error && _packet.localPort() == DDP_DEFAULT_PORT) { //DDP packet error = false; protocol = P_DDP; } if (!error) { - _callback(sbuff, _packet.remoteIP(), protocol); + _callback(sbuff, _packet.remoteIP(), protocol, pktLen); } } \ No newline at end of file diff --git a/wled00/src/dependencies/e131/ESPAsyncE131.h b/wled00/src/dependencies/e131/ESPAsyncE131.h index e0ddccfd67..4e77fdfbcd 100644 --- a/wled00/src/dependencies/e131/ESPAsyncE131.h +++ b/wled00/src/dependencies/e131/ESPAsyncE131.h @@ -55,19 +55,24 @@ typedef struct ip_addr ip4_addr_t; #define DDP_FLAGS_VER 0xc0 // version mask #define DDP_FLAGS_VER1 0x40 // version=1 #define DDP_FLAGS_PUSH 0x01 -#define DDP_FLAGS_QUERY 0x02 -#define DDP_FLAGS_REPLY 0x04 -#define DDP_FLAGS_STORAGE 0x08 +#define DDP_FLAGS_QUERY 0x02 // unsupported - used by XLights for auto-discovery +#define DDP_FLAGS_REPLY 0x04 // unsupported - response packet from another display +#define DDP_FLAGS_STORAGE 0x08 // unsupported - show data from a storage unit instead of from packet data field. Data field defines storage unit (by name, number, URL or whatever mechanism wanted). #define DDP_FLAGS_TIME 0x10 #define DDP_CHANNELS_PER_PACKET 1440 // 480 leds #define DDP_TYPE_RGB24 0x0B // 00 001 011 (RGB , 8 bits per channel, 3 channels) #define DDP_TYPE_RGBW32 0x1B // 00 011 011 (RGBW, 8 bits per channel, 4 channels) +#define DDP_TYPE_LEGACY 0x01 // 00 000 001 legacy RGB 8bit definition +#define DDP_TYPE_UNDEF 0x00 // type and bit depth undefined -#define DDP_ID_DISPLAY 1 -#define DDP_ID_CONFIG 250 -#define DDP_ID_STATUS 251 +// DDP Source or Destination ID (header byte 3) +#define DDP_ID_DISPLAY 1 // default output device +#define DDP_ID_CONTROL 246 // JSON control (not implemented) +#define DDP_ID_CONFIG 250 // JSON config (not implemented) +#define DDP_ID_STATUS 251 // JSON status (not implemented) +#define DDP_ID_ALL 255 // all devices #define ARTNET_OPCODE_OPDMX 0x5000 #define ARTNET_OPCODE_OPPOLL 0x2000 @@ -212,7 +217,7 @@ typedef union { } ArtPollReply; // new packet callback -typedef void (*e131_packet_callback_function) (e131_packet_t* p, IPAddress clientIP, byte protocol); +typedef void (*e131_packet_callback_function) (e131_packet_t* p, IPAddress clientIP, byte protocol, size_t packetLen); class ESPAsyncE131 { private: diff --git a/wled00/udp.cpp b/wled00/udp.cpp index 728f19e909..ed8bc6c8f3 100644 --- a/wled00/udp.cpp +++ b/wled00/udp.cpp @@ -804,6 +804,7 @@ uint8_t realtimeBroadcast(uint8_t type, IPAddress client, uint16_t length, const // write the header /*0*/ddpUdp.write(flags); + // TODO: sequence number should be 1-15 as 0 means "unused", it has no bad consequences other than out of sequence packet may be accepted /*1*/ddpUdp.write(sequenceNumber++ & 0x0F); // sequence may be unnecessary unless we are sending twice (as requested in Sync settings) /*2*/ddpUdp.write(isRGBW ? DDP_TYPE_RGBW32 : DDP_TYPE_RGB24); /*3*/ddpUdp.write(DDP_ID_DISPLAY); diff --git a/wled00/util.cpp b/wled00/util.cpp index 66aaea97fc..20c934b0f1 100644 --- a/wled00/util.cpp +++ b/wled00/util.cpp @@ -8,6 +8,7 @@ #else #include #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0) + #include "rom/rtc.h" // for rtc_get_reset_reason() #include "esp32/rtc.h" // for bootloop detection #elif ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(3, 3, 0) #include "soc/rtc.h" @@ -920,7 +921,7 @@ void *allocate_buffer(size_t size, uint32_t type) { buffer = p_malloc(size); // prefer PSRAM } else if (type & BFRALLOC_ENFORCE_PSRAM) - buffer = heap_caps_malloc(size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); // use PSRAM only, otherwise return nullptr + buffer = p_malloc(size); // use PSRAM if available, fall back to DRAM if not (safeguard for boards without PSRAM #5629) buffer = validateFreeHeap(buffer); #endif if (buffer && (type & BFRALLOC_CLEAR)) @@ -998,6 +999,11 @@ RTC_NOINIT_ATTR static uint32_t bl_crashcounter; RTC_NOINIT_ATTR static uint32_t bl_actiontracker; static inline ResetReason rebootReason() { + // check RTC restart reason first - brownout is not reliably reported by esp_reset_reason() + if (rtc_get_reset_reason(0) == RTCWDT_BROWN_OUT_RESET) return ResetReason::Brownout; // core0 brownout + #if SOC_CPU_CORES_NUM > 1 + if (rtc_get_reset_reason(1) == RTCWDT_BROWN_OUT_RESET) return ResetReason::Brownout; // core1 brownout + #endif esp_reset_reason_t reason = esp_reset_reason(); if (reason == ESP_RST_BROWNOUT) return ResetReason::Brownout; if (reason == ESP_RST_SW) return ResetReason::Software; @@ -1032,6 +1038,7 @@ static bool detectBootLoop() { case ResetReason::Crash: { DEBUG_PRINTLN(F("crash detected!")); + errorFlag = ERR_SYS_REBOOT; uint32_t rebootinterval = rtctime - bl_last_boottime; if (rebootinterval < BOOTLOOP_INTERVAL_MILLIS) { bl_crashcounter++; @@ -1052,6 +1059,7 @@ static bool detectBootLoop() { case ResetReason::Brownout: // crash due to brownout can't be detected unless using flash memory to store bootloop variables DEBUG_PRINTLN(F("brownout detected")); + errorFlag = ERR_SYS_BROWNOUT; //restoreConfig(); // TODO: blindly restoring config if brownout detected is a bad idea, need a better way (if at all) break; } @@ -1296,6 +1304,9 @@ String computeSHA1(const String& input) { #ifdef ESP32 #include "esp_adc_cal.h" +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(4,4,7) // backwards compatibility patch + #define ADC_ATTEN_DB_12 ADC_ATTEN_DB_11 +#endif String generateDeviceFingerprint() { uint32_t fp[2] = {0, 0}; // create 64 bit fingerprint esp_chip_info_t chip_info; diff --git a/wled00/wled.cpp b/wled00/wled.cpp index eb6019e6bf..6205706488 100644 --- a/wled00/wled.cpp +++ b/wled00/wled.cpp @@ -178,7 +178,7 @@ void WLED::loop() // calling getContiguousFreeHeap() during led update causes glitches on C3 // this can (probably) be removed once RMT driver for C3 is fixed unsigned t0 = millis(); - while (strip.isUpdating() && (millis() - t0 < 15)) delay(1); // be nice, but not too nice. Waits up to 15ms + while (strip.isUpdating() && (millis() - t0 < 150)) delay(1); // be nice, but not too nice. Waits up to 150ms #endif uint32_t heap = getContiguousFreeHeap(); // ESP32 family needs ~10k of contiguous free heap for UI to work properly #endif @@ -610,31 +610,35 @@ void WLED::beginStrip() // init offMode and relay offMode = false; // init to on state to allow proper relay init handleOnOff(true); // init relay and force off + if (rlyPin < 0) strip.show(); // ensure LEDs are off if no relay is used + // Note on how bootup behaviour works: + // if turnOnAtBoot is false: strip is set to black. It will fade in to startup brightness and orange when turned on + // if a bootup preset is set, it will fade to that preset if it has "on:true" set (to default brightness) or to that preset's brightness if set + // if turnOnAtBoot is true: the LEDs will fade in to orange and default brightness + // if a bootup preset is set, it will start at the default brightness except if "fade" transition is used, then it will still fade from black + // there is no way to have LEDs off at boot and upon turn-on have them immediatel jump to a target brightness but users can use a playlist to do that + + bri = 0; // start off black by default (on a fresh install this is overruled by briS as turnOnAtBoot is true) if (turnOnAtBoot) { - if (briS > 0) bri = briS; - else if (bri == 0) bri = 128; - } else { - // fix for #3196 - if (bootPreset > 0) { - // set all segments black (no transition) - for (unsigned i = 0; i < strip.getSegmentsNum(); i++) { - Segment &seg = strip.getSegment(i); - if (seg.isActive()) seg.colors[0] = BLACK; - } - colPri[0] = colPri[1] = colPri[2] = colPri[3] = 0; // needed for colorUpdated() - } - briLast = briS; bri = 0; - strip.fill(BLACK); - if (rlyPin < 0) - strip.show(); // ensure LEDs are off if no relay is used + bri = briS; // load startup brightness (set in UI), 0 is not allowed in UI } - colorUpdated(CALL_MODE_INIT); // will not send notification but will initiate transition + else briLast = briS; // go to startup brightness (set in UI) when turning on (can be overruled by a preset) + colorUpdated(CALL_MODE_INIT); // set bootup brightness immediately, do not send notification (brightness is also set for preset if used, useful for swipe etc.) + if (bootPreset > 0) { applyPreset(bootPreset, CALL_MODE_INIT); } + else { + // set color to warm welcoming orange (aka DEFAULT_COLOR) if no preset loaded (will fade to this color once turned on) + colPri[0] = R(DEFAULT_COLOR); + colPri[1] = G(DEFAULT_COLOR); + colPri[2] = B(DEFAULT_COLOR); + colPri[3] = W(DEFAULT_COLOR); + } - strip.setTransition(transitionDelayDefault); // restore transitions + strip.setTransition(transitionDelayDefault); // restore default transition time + colorUpdated(CALL_MODE_INIT); // apply color & initiate transition, do not send notification } void WLED::initAP(bool resetAP) @@ -651,6 +655,7 @@ void WLED::initAP(bool resetAP) WiFi.softAPConfig(IPAddress(4, 3, 2, 1), IPAddress(4, 3, 2, 1), IPAddress(255, 255, 255, 0)); WiFi.softAP(apSSID, apPass, apChannel, apHide); #ifdef ARDUINO_ARCH_ESP32 + DEBUG_PRINT(F("access point maxTxPower set to ")); DEBUG_PRINTLN(txPower); WiFi.setTxPower(wifi_power_t(txPower)); #endif @@ -691,6 +696,7 @@ void WLED::initConnection() } #endif + DEBUG_PRINTLN(F("WiFi disconnect.")); WiFi.disconnect(true); // close old connections delay(5); // wait for hardware to be ready #ifdef ESP8266 @@ -703,13 +709,14 @@ void WLED::initConnection() #ifdef ARDUINO_ARCH_ESP32 // Reset mode to NULL to force a full STA mode transition, so that WiFi.mode(WIFI_STA) below actually applies the hostname (and TX power, etc.). // This is required on reconnects when mode is already WIFI_STA. + DEBUG_PRINTLN(F("WiFi mode_null: driver teardown / re-init.")); WiFi.mode(WIFI_MODE_NULL); apActive = false; // the AP is physically torn down by WIFI_MODE_NULL delay(5); // give the WiFi stack time to complete the mode transition WiFi.setHostname(hostname); #endif - if (multiWiFi[selectedWiFi].staticIP != 0U && multiWiFi[selectedWiFi].staticGW != 0U) { + if (uint32_t(multiWiFi[selectedWiFi].staticIP) != 0U && uint32_t(multiWiFi[selectedWiFi].staticGW) != 0U) { // explicit cast to uint32_t ensures we check the IPv4 adress, not IPv6 WiFi.config(multiWiFi[selectedWiFi].staticIP, multiWiFi[selectedWiFi].staticGW, multiWiFi[selectedWiFi].staticSN, dnsAddress); } else { WiFi.config(IPAddress((uint32_t)0), IPAddress((uint32_t)0), IPAddress((uint32_t)0)); @@ -786,9 +793,12 @@ void WLED::initConnection() #endif // WLED_ENABLE_WPA_ENTERPRISE #ifdef ARDUINO_ARCH_ESP32 + DEBUG_PRINT(F("WiFi maxTxPower set to ")); DEBUG_PRINT(txPower); + DEBUG_PRINT(F("; WiFi sleep ")); DEBUG_PRINTLN(noWifiSleep ? F("disabled."):F("enabled.")); WiFi.setTxPower(wifi_power_t(txPower)); WiFi.setSleep(!noWifiSleep); #else // ESP8266 accepts a hostname set after WiFi interface initialization + DEBUG_PRINT(F("WiFi sleep ")); DEBUG_PRINTLN(noWifiSleep ? F("disabled."):F("enabled.")); wifi_set_sleep_type((noWifiSleep) ? NONE_SLEEP_T : MODEM_SLEEP_T); WiFi.hostname(hostname); #endif diff --git a/wled00/wled.h b/wled00/wled.h index 1a5f1b143e..ce154f5653 100644 --- a/wled00/wled.h +++ b/wled00/wled.h @@ -7,7 +7,7 @@ */ // version code in format yymmddb (b = daily build) -#define VERSION 2605011 +#define VERSION 2606301 //uncomment this if you have a "my_config.h" file you'd like to use //#define WLED_USE_MY_CONFIG @@ -413,8 +413,8 @@ WLED_GLOBAL bool gammaCorrectCol _INIT(true); // use gamma correction on col WLED_GLOBAL bool gammaCorrectBri _INIT(false); // use gamma correction on brightness WLED_GLOBAL float gammaCorrectVal _INIT(2.2f); // gamma correction value -WLED_GLOBAL byte colPri[] _INIT_N(({ 255, 160, 0, 0 })); // current RGB(W) primary color. colPri[] should be updated if you want to change the color. -WLED_GLOBAL byte colSec[] _INIT_N(({ 0, 0, 0, 0 })); // current RGB(W) secondary color +WLED_GLOBAL byte colPri[] _INIT_N(({ 0, 0, 0, 0 })); // current RGB(W) primary color. colPri[] should be updated if you want to change the color. +WLED_GLOBAL byte colSec[] _INIT_N(({ 0, 0, 0, 0 })); // current RGB(W) secondary color WLED_GLOBAL byte nightlightTargetBri _INIT(0); // brightness after nightlight is over WLED_GLOBAL byte nightlightDelayMins _INIT(60); @@ -472,7 +472,11 @@ WLED_GLOBAL bool arlsForceMaxBri _INIT(false); // enable to f WLED_GLOBAL int dmxInputTransmitPin _INIT(-1); WLED_GLOBAL int dmxInputReceivePin _INIT(-1); WLED_GLOBAL int dmxInputEnablePin _INIT(-1); + #if defined(ARDUINO_ARCH_ESP32) && (SOC_UART_NUM > 2) WLED_GLOBAL int dmxInputPort _INIT(2); + #else + WLED_GLOBAL int dmxInputPort _INIT(1); // some MCUs only have two UART units + #endif WLED_GLOBAL DMXInput dmxInput; #endif @@ -776,7 +780,7 @@ typedef class Udp { RgbConnected = c3; } } __attribute__ ((aligned(1), packed)) udp_port_t; -WLED_GLOBAL udp_port_t udp _INIT_N(({21234, 65506, 19446, 0, false, false, false})); +WLED_GLOBAL udp_port_t udp _INIT_N(({21324, 65506, 19446, 0, false, false, false})); #define udpPort udp.Port #define udpPort2 udp.Port2 #define udpRgbPort udp.RgbPort diff --git a/wled00/wled_server.cpp b/wled00/wled_server.cpp index 0b4d0fb546..d55ae79bfe 100644 --- a/wled00/wled_server.cpp +++ b/wled00/wled_server.cpp @@ -7,7 +7,9 @@ #include "html_settings.h" #include "html_other.h" #include "js_iro.h" -#include "js_omggif.h" +#ifdef WLED_ENABLE_GIF + #include "js_omggif.h" +#endif #ifdef WLED_ENABLE_PIXART #include "html_pixart.h" #endif @@ -42,7 +44,9 @@ static const char s_no_store[] PROGMEM = "no-store"; static const char s_expires[] PROGMEM = "Expires"; static const char _common_js[] PROGMEM = "/common.js"; static const char _iro_js[] PROGMEM = "/iro.js"; +#ifdef WLED_ENABLE_GIF static const char _omggif_js[] PROGMEM = "/omggif.js"; +#endif //Is this an IP? static bool isIp(const String &str) { @@ -362,9 +366,11 @@ void initServer() handleStaticContent(request, FPSTR(_iro_js), 200, FPSTR(CONTENT_TYPE_JAVASCRIPT), JS_iro, JS_iro_length); }); +#ifdef WLED_ENABLE_GIF server.on(_omggif_js, HTTP_GET, [](AsyncWebServerRequest *request) { handleStaticContent(request, FPSTR(_omggif_js), 200, FPSTR(CONTENT_TYPE_JAVASCRIPT), JS_omggif, JS_omggif_length); }); +#endif //settings page server.on(F("/settings"), HTTP_GET, [](AsyncWebServerRequest *request){ diff --git a/wled00/ws.cpp b/wled00/ws.cpp index a73bc297ec..6e9038c101 100644 --- a/wled00/ws.cpp +++ b/wled00/ws.cpp @@ -87,19 +87,13 @@ void wsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventTyp if (!data || len < offset+1) return; // catch invalid / single-byte payload switch (data[0]) { case BINARY_PROTOCOL_E131: - handleE131Packet((e131_packet_t*)&data[offset], client->remoteIP(), P_E131); + handleE131Packet((e131_packet_t*)&data[offset], client->remoteIP(), P_E131, len - offset); break; case BINARY_PROTOCOL_ARTNET: - handleE131Packet((e131_packet_t*)&data[offset], client->remoteIP(), P_ARTNET); + handleE131Packet((e131_packet_t*)&data[offset], client->remoteIP(), P_ARTNET, len - offset); break; case BINARY_PROTOCOL_DDP: - if (len < 10 + offset) return; // DDP header is 10 bytes (+1 protocol byte) - size_t ddpDataLen = (data[8+offset] << 8) | data[9+offset]; // data length in bytes from DDP header - uint8_t flags = data[0+offset]; - if ((flags & DDP_FLAGS_TIME) ) ddpDataLen += 4; // timecode flag adds 4 bytes to data length - if (len < (10 + offset + ddpDataLen)) return; // not enough data, prevent out of bounds read - // could be a valid DDP packet, forward to handler - handleE131Packet((e131_packet_t*)&data[offset], client->remoteIP(), P_DDP); + handleE131Packet((e131_packet_t*)&data[offset], client->remoteIP(), P_DDP, len - offset); } } } else { diff --git a/wled00/xml.cpp b/wled00/xml.cpp index 812ef8c207..03d4cd1101 100644 --- a/wled00/xml.cpp +++ b/wled00/xml.cpp @@ -628,7 +628,7 @@ void getSettingsJS(byte subPage, Print& settingsScript) printSetFormValue(settingsScript,PSTR("MN"),macroNl); int ii = 0; for (const auto &button : buttons) { - settingsScript.printf_P(PSTR("addRow(%d,%d,%d,%d);"), ii++, button.macroButton, button.macroLongPress, button.macroDoublePress); + settingsScript.printf_P(PSTR("addRow(%d,%d,%d,%d,%d);"), ii++, button.macroButton, button.macroLongPress, button.macroDoublePress, button.type); } settingsScript.printf_P(PSTR("maxTimers=%d;"), WLED_MAX_TIMERS);