mirror of
https://github.com/wled/WLED.git
synced 2026-06-26 06:43:18 +00:00
Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f5adaaa44e | |||
| 78c1051cbb | |||
| 121250c0a0 | |||
| 2302863386 | |||
| 7560811480 | |||
| 252007d37f | |||
| 6e76a69417 | |||
| 24e2e3c195 | |||
| f1c58ee75f | |||
| c8fc1c89b6 | |||
| 12f6fbc005 | |||
| c819814904 | |||
| 0ef5ee74d4 | |||
| 51862e3572 | |||
| aaf51927a6 | |||
| 72a43c6431 | |||
| 7c6828d443 | |||
| f8f46736ec | |||
| e4f8534cf0 | |||
| 79ecffe0f3 | |||
| 1fb636d8ea | |||
| e04c855e6c | |||
| 6a627a86b7 | |||
| 746df24011 | |||
| da64f71ce2 | |||
| 8972b6bfe2 | |||
| 8241468fe2 | |||
| e9b740a915 | |||
| 548bb6ffd5 | |||
| a5f28d0fcb | |||
| fb8f8f0b08 | |||
| 53fdf9a89c | |||
| a1316034c1 | |||
| 75df4affa8 | |||
| 820c841376 | |||
| 34d50710b3 | |||
| d0d62d9493 | |||
| 89b2bc2992 | |||
| e019d36221 | |||
| 3bcb0c902e | |||
| 75b679814b |
@@ -14,19 +14,6 @@
|
||||
language: en-US
|
||||
|
||||
reviews:
|
||||
# generic review setting, see https://docs.coderabbit.ai/reference/configuration#reference
|
||||
auto_apply_labels: true
|
||||
# abort_on_close: false
|
||||
high_level_summary: true
|
||||
review_status: true
|
||||
collapse_walkthrough: false
|
||||
poem: false
|
||||
# sequence_diagrams: false
|
||||
auto_review:
|
||||
enabled: true
|
||||
ignore_title_keywords:
|
||||
- WIP
|
||||
|
||||
path_instructions:
|
||||
- path: "**/*.{cpp,h,hpp,ino}"
|
||||
instructions: >
|
||||
@@ -39,8 +26,6 @@ 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).
|
||||
|
||||
- path: "wled00/data/**"
|
||||
instructions: >
|
||||
@@ -49,8 +34,6 @@ 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).
|
||||
|
||||
- path: "wled00/html_*.h"
|
||||
instructions: >
|
||||
@@ -70,8 +53,6 @@ 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).
|
||||
|
||||
- path: ".github/workflows/*.{yml,yaml}"
|
||||
instructions: >
|
||||
@@ -83,8 +64,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: |
|
||||
@@ -101,13 +80,3 @@ reviews:
|
||||
AI-facing rules due to edits introduced in this PR.
|
||||
3. If new AI-facing rules were added without updating a related HUMAN_ONLY
|
||||
reference section, note this as a suggestion (not a required fix).
|
||||
|
||||
finishing_touches:
|
||||
# Docstrings | Options for generating Docstrings for your PRs/MRs.
|
||||
docstrings:
|
||||
# Docstrings | Allow CodeRabbit to generate docstrings for PRs/MRs.
|
||||
# default: true - disabled in WLED: has caused confusion in the past
|
||||
enabled: false
|
||||
unit_tests:
|
||||
# default: true - disabled in WLED: we don't have a unit test framework, this option just confuses contributors
|
||||
enabled: false
|
||||
|
||||
@@ -112,9 +112,7 @@ Match this workflow in local development to catch failures before pushing.
|
||||
## Important Reminders
|
||||
|
||||
- Always **commit source code**
|
||||
- Every pull request MUST include a clear description of *what* changed and *why*.
|
||||
- **Never edit or commit** `wled00/html_*.h` and `wled00/js_*.h` — auto-generated from `wled00/data/`
|
||||
- After modifying source code files, check that any **previous comments have been preserved** or updated to reflect the new behaviour.
|
||||
- Web UI rebuild is part of the PlatformIO firmware compilation pipeline
|
||||
- Common environments: `nodemcuv2`, `esp32dev`, `esp8266_2m`, `esp32c3dev`, `esp32s3dev_8MB_opi`
|
||||
- List all PlatformIO targets: `pio run --list-targets`
|
||||
|
||||
@@ -108,13 +108,10 @@ docs/ # Contributor docs, coding guidelines
|
||||
- **Never edit or commit** `wled00/html_*.h` and `wled00/js_*.h` — auto-generated from `wled00/data/`.
|
||||
- If updating Web UI files in `wled00/data/`, **make use of common functions in `wled00/data/common.js` whenever possible**.
|
||||
- **When unsure, say so.** Gather more information rather than guessing.
|
||||
- **Acknowledge good patterns** when you see them. Positive feedback always helps.
|
||||
- **Acknowledge good patterns** when you see them. Summarize good practices as part of your review - positive feedback always helps.
|
||||
- **Provide references** when making analyses or recommendations. Base them on the correct branch or PR.
|
||||
- **Highlight user-visible breaking changes and ripple effects**. Ask for confirmation that these were introduced intentionally.
|
||||
- **Unused / dead code must be justified or removed**. This helps to keep the codebase clean, maintainable and readable.
|
||||
- **Verify feature-flag names.** Every `WLED_ENABLE_*` / `WLED_DISABLE_*` flag must exactly match one of the names below — misspellings are silently ignored by the preprocessor (e.g. `WLED_IR_DISABLE` instead of `WLED_DISABLE_INFRARED`), causing silent build variations. Flag unrecognised names as likely typos and suggest the correct spelling.
|
||||
<br>**`WLED_DISABLE_*`**: `2D`, `ADALIGHT`, `ALEXA`, `BROWNOUT_DET`, `ESPNOW`, `FILESYSTEM`, `HUESYNC`, `IMPROV_WIFISCAN`, `INFRARED`, `LOXONE`, `MQTT`, `OTA`, `PARTICLESYSTEM1D`, `PARTICLESYSTEM2D`, `PIXELFORGE`, `WEBSOCKETS`
|
||||
<br>**`WLED_ENABLE_*`**: `ADALIGHT`, `AOTA`, `DMX`, `DMX_INPUT`, `DMX_OUTPUT`, `FS_EDITOR`, `GIF`, `HUB75MATRIX`, `JSONLIVE`, `LOXONE`, `MQTT`, `PIXART`, `PXMAGIC`, `USERMOD_PAGE`, `WEBSOCKETS`, `WPA_ENTERPRISE`
|
||||
- **C++ formatting available**: `clang-format` is installed but not in CI
|
||||
- No automated linting is configured — match existing code style in files you edit.
|
||||
|
||||
@@ -123,14 +120,13 @@ Refer to `docs/cpp.instructions.md` and `docs/web.instructions.md` for language-
|
||||
### 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).
|
||||
- When a larger block of code is generated by an AI tool, mark it with an `// AI: below section was generated by an AI` comment (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
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
; ----------------------------------------------------------------------------
|
||||
; platformio_release.ini.template
|
||||
; ----------------------------------------------------------------------------
|
||||
; Copied to platformio_release.ini by the release CI workflow
|
||||
; (.github/workflows/release.yml -> build.yml with `release: true`)
|
||||
; in order to extend the matrix of `default_envs` built and published
|
||||
; for tagged releases.
|
||||
;
|
||||
; This file overrides `[platformio].default_envs` from platformio.ini via
|
||||
; `extra_configs`. It MUST list every env that should be released - including
|
||||
; the regular CI default_envs - because it fully replaces the parent value.
|
||||
;
|
||||
; Do NOT commit a generated platformio_release.ini (it's in .gitignore).
|
||||
; ----------------------------------------------------------------------------
|
||||
|
||||
[platformio]
|
||||
default_envs = nodemcuv2
|
||||
esp8266_2m
|
||||
esp01_1m_full
|
||||
nodemcuv2_160
|
||||
esp8266_2m_160
|
||||
esp01_1m_full_160
|
||||
nodemcuv2_compat
|
||||
esp8266_2m_compat
|
||||
esp01_1m_full_compat
|
||||
esp32dev
|
||||
esp32dev_debug
|
||||
esp32_eth
|
||||
esp32_wrover
|
||||
lolin_s2_mini
|
||||
esp32c3dev
|
||||
esp32c3dev_qio
|
||||
esp32S3_wroom2
|
||||
esp32s3dev_16MB_opi
|
||||
esp32s3dev_8MB_opi
|
||||
esp32s3dev_8MB_qspi
|
||||
esp32s3_4M_qspi
|
||||
usermods
|
||||
; HUB75 release-only envs
|
||||
esp32dev_hub75
|
||||
esp32dev_hub75_forum_pinout
|
||||
adafruit_matrixportal_esp32s3
|
||||
esp32s3dev_16MB_opi_hub75 ;; MoonHub
|
||||
esp32s3dev_4MB_qspi_hub75 ;; HD-WF2
|
||||
@@ -3,15 +3,7 @@ name: WLED Build
|
||||
# Only included into other workflows
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
release:
|
||||
description: 'Build the release env matrix (uses .github/platformio_release.ini.template)'
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
|
||||
jobs:
|
||||
|
||||
get_default_envs:
|
||||
@@ -19,9 +11,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Apply release config
|
||||
if: inputs.release
|
||||
run: cp .github/platformio_release.ini.template platformio_release.ini
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
@@ -46,9 +35,6 @@ jobs:
|
||||
environment: ${{ fromJSON(needs.get_default_envs.outputs.environments) }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Apply release config
|
||||
if: inputs.release
|
||||
run: cp .github/platformio_release.ini.template platformio_release.ini
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
@@ -65,8 +51,8 @@ jobs:
|
||||
~/.platformio/.cache
|
||||
~/.buildcache
|
||||
build_output
|
||||
key: pio-${{ runner.os }}-${{ matrix.environment }}-${{ hashFiles('platformio.ini', '.github/platformio_release.ini.template', 'pio-scripts/output_bins.py') }}-${{ hashFiles('wled00/**', 'usermods/**') }}
|
||||
restore-keys: pio-${{ runner.os }}-${{ matrix.environment }}-${{ hashFiles('platformio.ini', '.github/platformio_release.ini.template', 'pio-scripts/output_bins.py') }}-
|
||||
key: pio-${{ runner.os }}-${{ matrix.environment }}-${{ hashFiles('platformio.ini', 'pio-scripts/output_bins.py') }}-${{ hashFiles('wled00/**', 'usermods/**') }}
|
||||
restore-keys: pio-${{ runner.os }}-${{ matrix.environment }}-${{ hashFiles('platformio.ini', 'pio-scripts/output_bins.py') }}-
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
@@ -77,21 +63,9 @@ jobs:
|
||||
|
||||
- name: Build firmware
|
||||
run: pio run -e ${{ matrix.environment }}
|
||||
- name: Get artifact name from bin filename
|
||||
id: artifact_name
|
||||
run: |
|
||||
bin=$(ls build_output/release/*.bin 2>/dev/null | head -1)
|
||||
if [ -n "$bin" ]; then
|
||||
# Strip WLED_<version>_ prefix from WLED_<version>_<RELEASE_NAME>.bin
|
||||
base=$(basename "$bin" .bin)
|
||||
release_name=$(echo "$base" | sed 's/^[^_]*_[^_]*_//')
|
||||
echo "name=firmware-$release_name" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "name=firmware-${{ matrix.environment }}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ steps.artifact_name.outputs.name }}
|
||||
name: firmware-${{ matrix.environment }}
|
||||
path: |
|
||||
build_output/release/*.bin
|
||||
build_output/release/*_ESP02*.bin.gz
|
||||
|
||||
@@ -7,9 +7,6 @@ on:
|
||||
# This can be used to allow manually triggering nightlies from the web interface
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
jobs:
|
||||
wled_build:
|
||||
uses: ./.github/workflows/build.yml
|
||||
|
||||
@@ -5,15 +5,10 @@ on:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
jobs:
|
||||
|
||||
wled_build:
|
||||
uses: ./.github/workflows/build.yml
|
||||
with:
|
||||
release: true
|
||||
|
||||
release:
|
||||
name: Create Release
|
||||
@@ -23,14 +18,6 @@ jobs:
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
merge-multiple: true
|
||||
- name: Create draft release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
body: "Release assets uploaded. Release notes pending..."
|
||||
draft: True
|
||||
files: |
|
||||
*.bin
|
||||
*.bin.gz
|
||||
- name: "✏️ Generate release changelog"
|
||||
id: changelog
|
||||
uses: janheinrichmerker/action-github-changelog-generator@v2.4
|
||||
@@ -40,9 +27,12 @@ jobs:
|
||||
maxIssues: 500
|
||||
# Exclude issues that were closed without resolution from changelog
|
||||
excludeLabels: 'stale,wontfix,duplicate,invalid,external,question,use-as-is,not_planned'
|
||||
- name: Update release description
|
||||
- name: Create draft release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
body: ${{ steps.changelog.outputs.changelog }}
|
||||
draft: True
|
||||
files: |
|
||||
*.bin
|
||||
*.bin.gz
|
||||
|
||||
|
||||
@@ -4,10 +4,7 @@ on:
|
||||
pull_request:
|
||||
paths:
|
||||
- usermods/**
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
|
||||
jobs:
|
||||
|
||||
get_usermod_envs:
|
||||
|
||||
@@ -16,7 +16,6 @@ __pycache__/
|
||||
|
||||
esp01-update.sh
|
||||
platformio_override.ini
|
||||
platformio_release.ini
|
||||
replace_fs.py
|
||||
wled-update.sh
|
||||
|
||||
|
||||
@@ -1,184 +0,0 @@
|
||||
# AGENTS.md — WLED Coding Agent Reference
|
||||
|
||||
WLED is C++ firmware for ESP32/ESP8266 microcontrollers controlling addressable LEDs,
|
||||
with a web UI (HTML/JS/CSS). Built with PlatformIO (Arduino framework) and Node.js tooling.
|
||||
|
||||
See also: `.github/copilot-instructions.md`, `.github/agent-build.instructions.md`,
|
||||
`docs/cpp.instructions.md`, `docs/web.instructions.md`, `docs/cicd.instructions.md`.
|
||||
|
||||
## Build Commands
|
||||
|
||||
| Command | Purpose | Timeout |
|
||||
|---|---|---|
|
||||
| `npm ci` | Install Node.js deps (required first) | 30s |
|
||||
| `npm run build` | Build web UI into `wled00/html_*.h` / `wled00/js_*.h` | 30s |
|
||||
| `npm test` | Run test suite (Node.js built-in `node --test`) | 2 min |
|
||||
| `npm run dev` | Watch mode — auto-rebuilds web UI on changes | continuous |
|
||||
| `pio run -e esp32dev` | Build firmware (ESP32, most common target) | 30 min |
|
||||
| `pio run -e nodemcuv2` | Build firmware (ESP8266) | 30 min |
|
||||
|
||||
**Always run `npm ci && npm run build` before `pio run`.** The web UI build generates
|
||||
required C headers for firmware compilation.
|
||||
|
||||
### Running a Single Test
|
||||
|
||||
Tests use Node.js built-in test runner (`node:test`). The single test file is
|
||||
`tools/cdata-test.js`. Run it with:
|
||||
|
||||
```sh
|
||||
npm test # runs all tests via `node --test`
|
||||
node --test tools/cdata-test.js # run just that file directly
|
||||
```
|
||||
|
||||
There are no C++ unit tests. Firmware is validated by successful compilation across
|
||||
target environments. Always build after code changes: `pio run -e esp32dev`.
|
||||
|
||||
### Common Firmware Environments
|
||||
|
||||
`esp32dev`, `nodemcuv2`, `esp8266_2m`, `esp32c3dev`, `esp32s3dev_8MB_opi`, `lolin_s2_mini`
|
||||
|
||||
### Recovery / Troubleshooting
|
||||
|
||||
```sh
|
||||
npm run build -- -f # force web UI rebuild
|
||||
rm -f wled00/html_*.h wled00/js_*.h && npm run build # clean + rebuild UI
|
||||
pio run --target clean # clean PlatformIO build artifacts
|
||||
rm -rf node_modules && npm ci # reinstall Node.js deps
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
wled00/ # Main firmware source (C++)
|
||||
data/ # Web UI source (HTML/JS/CSS) — tabs for indentation
|
||||
html_*.h, js_*.h # Auto-generated (NEVER edit or commit)
|
||||
src/ # Sub-modules: fonts, bundled dependencies (ArduinoJSON)
|
||||
usermods/ # Community usermods (each has library.json + .cpp/.h)
|
||||
platformio.ini # Build configuration and environments
|
||||
pio-scripts/ # PlatformIO build scripts (Python)
|
||||
tools/ # Node.js build tools (cdata.js) and tests
|
||||
docs/ # Coding convention docs
|
||||
.github/workflows/ # CI/CD (GitHub Actions)
|
||||
```
|
||||
|
||||
## C++ Code Style (wled00/, usermods/)
|
||||
|
||||
### Formatting
|
||||
- **2-space indentation** (no tabs in C++ files)
|
||||
- K&R brace style preferred (opening brace on same line)
|
||||
- Single-statement `if` bodies may omit braces: `if (a == b) doStuff(a);`
|
||||
- Space after keywords (`if (...)`, `for (...)`), no space before function parens (`doStuff(a)`)
|
||||
- No enforced line-length limit
|
||||
|
||||
### Naming Conventions
|
||||
| Kind | Convention | Examples |
|
||||
|---|---|---|
|
||||
| Functions, variables | camelCase | `setRandomColor()`, `effectCurrent` |
|
||||
| Classes, structs | PascalCase | `BusConfig`, `UsermodTemperature` |
|
||||
| Macros, constants | UPPER_CASE | `WLED_MAX_USERMODS`, `FX_MODE_STATIC` |
|
||||
| Private members | _camelCase | `_type`, `_bri`, `_len` |
|
||||
| Enum values | PascalCase | `PinOwner::BusDigital` |
|
||||
|
||||
### Includes
|
||||
- Include `"wled.h"` as the primary project header
|
||||
- Project headers first, then platform/Arduino, then third-party
|
||||
- Platform-conditional includes wrapped in `#ifdef ARDUINO_ARCH_ESP32` / `#ifdef ESP8266`
|
||||
|
||||
### Types and Const
|
||||
- Prefer `const &` for read-only function parameters
|
||||
- Mark getter/query methods `const`; use `static` for methods not accessing instance state
|
||||
- Prefer `constexpr` over `#define` for compile-time constants when possible
|
||||
- Use `static_assert` over `#if ... #error`
|
||||
- Use `uint_fast16_t` / `uint_fast8_t` in hot-path code
|
||||
|
||||
### Error Handling
|
||||
- **No C++ exceptions** — some builds disable them
|
||||
- Use return codes (`false`, `-1`) and global flags (`errorFlag = ERR_LOW_MEM`)
|
||||
- Use early returns as guard clauses: `if (!enabled || (strip.isUpdating() && (millis() - last_time < MAX_USERMOD_DELAY))) return;`
|
||||
- Debug output: `DEBUG_PRINTF()` / `DEBUG_PRINTLN()` (compiled out unless `-D WLED_DEBUG`)
|
||||
|
||||
### Strings and Memory
|
||||
- Use `F("string")` for string constants (saves RAM on ESP8266)
|
||||
- Use `PSTR()` with `DEBUG_PRINTF_P()` for format strings
|
||||
- Avoid `String` in hot paths; acceptable in config/setup code
|
||||
- Use `d_malloc()` (DRAM-preferred) / `p_malloc()` (PSRAM-preferred) for allocation
|
||||
- No VLAs — use fixed arrays or heap allocation
|
||||
- Call `reserve()` on strings/vectors to pre-allocate and avoid fragmentation
|
||||
|
||||
### Preprocessor / Feature Flags
|
||||
- Feature toggling: `WLED_DISABLE_*` and `WLED_ENABLE_*` flags (exact names matter!)
|
||||
- `WLED_DISABLE_*`: `2D`, `ADALIGHT`, `ALEXA`, `MQTT`, `OTA`, `INFRARED`, `WEBSOCKETS`, etc.
|
||||
- `WLED_ENABLE_*`: `DMX`, `GIF`, `HUB75MATRIX`, `JSONLIVE`, `WEBSOCKETS`, etc.
|
||||
- Platform: `ARDUINO_ARCH_ESP32`, `ESP8266`, `CONFIG_IDF_TARGET_ESP32S3`
|
||||
|
||||
### Comments
|
||||
- `//` for inline (always space after), `/* */` for block comments
|
||||
- AI-generated blocks: mark with `// AI: below section was generated by an AI` / `// AI: end`
|
||||
|
||||
### Math Functions
|
||||
- Use `sin8_t()`, `cos8_t()` — NOT `sin8()`, `cos8()` (removed, won't compile)
|
||||
- Use `sin_approx()` / `cos_approx()` instead of `sinf()` / `cosf()`
|
||||
- Replace `inoise8` / `inoise16` with `perlin8` / `perlin16`
|
||||
|
||||
### Hot-Path Code (Pixel Pipeline)
|
||||
- Use function attributes: `IRAM_ATTR`, `WLED_O2_ATTR`, `__attribute__((hot))`
|
||||
- Cache class members to locals before loops
|
||||
- Pre-compute invariants outside loops; use reciprocals to avoid division
|
||||
- Unsigned range checks: `if ((uint_fast16_t)(pix - start) < len)`
|
||||
|
||||
### ESP32 Tasks
|
||||
- `delay(1)` in custom FreeRTOS tasks (NOT `yield()`) — feeds IDLE watchdog
|
||||
- Do not use `delay()` in effects (FX.cpp) or hot pixel path
|
||||
|
||||
## Web UI Code Style (wled00/data/)
|
||||
|
||||
- **Tab indentation** for HTML, JS, and CSS
|
||||
- camelCase for JS functions/variables
|
||||
- Reuse helpers from `common.js` — do not duplicate utilities
|
||||
- After editing, run `npm run build` to regenerate headers
|
||||
- **Never edit** `wled00/html_*.h` or `wled00/js_*.h` directly
|
||||
|
||||
## Usermod Pattern
|
||||
|
||||
Usermods live in `usermods/<name>/` with a `.cpp`, optional `.h`, `library.json`, and `readme.md`.
|
||||
|
||||
```cpp
|
||||
class MyUsermod : public Usermod {
|
||||
private:
|
||||
bool enabled = false;
|
||||
static const char _name[];
|
||||
public:
|
||||
void setup() override { /* ... */ }
|
||||
void loop() override { /* ... */ }
|
||||
void addToConfig(JsonObject& root) override { /* ... */ }
|
||||
bool readFromConfig(JsonObject& root) override { /* ... */ }
|
||||
uint16_t getId() override { return USERMOD_ID_MYMOD; }
|
||||
};
|
||||
const char MyUsermod::_name[] PROGMEM = "MyUsermod";
|
||||
static MyUsermod myUsermod;
|
||||
REGISTER_USERMOD(myUsermod);
|
||||
```
|
||||
|
||||
- Add usermod IDs to `wled00/const.h`
|
||||
- Activate via `custom_usermods` in platformio build config
|
||||
- Base new usermods on `usermods/EXAMPLE/` (never edit the example directly)
|
||||
- Store repeated strings as `static const char[] PROGMEM`
|
||||
|
||||
## CI/CD
|
||||
|
||||
CI runs on every push/PR via GitHub Actions (`.github/workflows/wled-ci.yml`):
|
||||
1. `npm test` (web UI build validation)
|
||||
2. Firmware compilation for all default environments (~22 targets)
|
||||
3. Post-link validation of usermod linkage (`validate_modules.py`)
|
||||
|
||||
No automated linting is configured. Match existing code style in files you edit.
|
||||
|
||||
## General Rules
|
||||
|
||||
- Repository language is English
|
||||
- Never edit or commit auto-generated `wled00/html_*.h` / `wled00/js_*.h`
|
||||
- When updating an existing PR, retain the original description. Only modify it to ensure technical accuracy. Add change logs after the existing description.
|
||||
- No force-push on open PRs
|
||||
- Changes to `platformio.ini` require maintainer approval
|
||||
- Remove dead/unused code — justify or delete it
|
||||
- Verify feature-flag spelling exactly (misspellings are silently ignored by preprocessor)
|
||||
+2
-3
@@ -30,8 +30,7 @@ This lets you update your PR if needed, while you can work on other tasks in 'ma
|
||||
|
||||
### Target branch for pull requests
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Please make all PRs against the `main` branch.
|
||||
Please make all PRs against the `main` branch.
|
||||
|
||||
### Describing your PR
|
||||
|
||||
@@ -168,7 +167,7 @@ AI tools are powerful but "often wrong" - your judgment is essential! 😊
|
||||
|
||||
- ✅ **Understand the code** - As the person contributing to WLED, make sure you understand exactly what the AI-generated source code does
|
||||
- ✅ **Review carefully** - AI can lose comments, introduce bugs, or make unnecessary changes
|
||||
- ✅ **Be transparent** - Add comments `// AI: below section was generated by an AI` ... `// AI: end` around larger chunks
|
||||
- ✅ **Be transparent** - Add a comment like `// This section was AI-generated` for larger chunks
|
||||
- ✅ **Use AI for translation** - AI is great for translating comments to English (but verify technical terms!)
|
||||
|
||||
### Code style
|
||||
|
||||
@@ -25,7 +25,6 @@ See also: [CONTRIBUTING.md](../CONTRIBUTING.md) for general style guidelines tha
|
||||
|
||||
- **camelCase** for functions and variables: `setValuesFromMainSeg()`, `effectCurrent`
|
||||
- **PascalCase** for classes and structs: `PinManagerClass`, `BusConfig`
|
||||
- **PascalCase** for enum values: `PinOwner::BusDigital`
|
||||
- **UPPER_CASE** for macros and constants: `WLED_MAX_USERMODS`, `DEFAULT_CLIENT_SSID`
|
||||
|
||||
## General
|
||||
@@ -58,7 +57,7 @@ Most headers use `#ifndef` / `#define` guards. Some newer headers add `#pragma o
|
||||
void calculateCRC(const uint8_t* data, size_t len) {
|
||||
...
|
||||
}
|
||||
// AI: end
|
||||
// AI: end of AI-generated section
|
||||
```
|
||||
|
||||
Single-line AI-assisted edits do not need the marker — use it when the AI produced a contiguous block that a human did not write line-by-line.
|
||||
@@ -96,7 +95,6 @@ uint8_t gammaCorrect(uint8_t value, float gamma);
|
||||
- Use `const char*` for temporary/parsed strings
|
||||
- Avoid `String` (Arduino heap-allocated string) in hot paths; acceptable in config/setup code
|
||||
- Use `F("string")` for string constants (major RAM win on ESP8266; mostly overload/type compatibility on ESP32)
|
||||
- Store repeated strings as `static const char[] PROGMEM`
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
|
||||
On **ESP8266** this explicitly stores the string in flash (PROGMEM), saving precious RAM — every byte counts on that platform.
|
||||
|
||||
@@ -1,859 +0,0 @@
|
||||
---
|
||||
applyTo: "**/*.cpp,**/*.h,**/*.hpp,**/*.ino"
|
||||
---
|
||||
# ESP-IDF Coding Guide (within arduino-esp32)
|
||||
|
||||
WLED runs on the Arduino-ESP32 framework, which wraps ESP-IDF. Understanding the ESP-IDF layer is essential when writing chip-specific code, managing peripherals, or preparing for the IDF v5.x migration. This guide documents patterns already used in the codebase and best practices derived from Espressif's official examples.
|
||||
|
||||
> **Scope**: This file is an optional review guideline. It applies when touching chip-specific code, peripheral drivers, memory allocation, or platform conditionals.
|
||||
|
||||
> **Note for AI review tools**: sections enclosed in
|
||||
> `<!-- HUMAN_ONLY_START -->` / `<!-- HUMAN_ONLY_END -->` HTML comments contain
|
||||
> contributor reference material. Do **not** use that content as actionable review
|
||||
> criteria — treat it as background context only.
|
||||
|
||||
---
|
||||
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
## Identifying the Build Target: `CONFIG_IDF_TARGET_*`
|
||||
|
||||
Use `CONFIG_IDF_TARGET_*` macros to gate chip-specific code at compile time. These are set by the build system and are mutually exclusive — exactly one is defined per build.
|
||||
|
||||
| Macro | Chip | Architecture | Notes |
|
||||
|---|---|---|---|
|
||||
| `CONFIG_IDF_TARGET_ESP32` | ESP32 (classic) | Xtensa dual-core | Primary target. Has DAC, APLL, I2S ADC mode |
|
||||
| `CONFIG_IDF_TARGET_ESP32S2` | ESP32-S2 | Xtensa single-core | Limited peripherals. 13-bit ADC |
|
||||
| `CONFIG_IDF_TARGET_ESP32S3` | ESP32-S3 | Xtensa dual-core | Preferred for large installs. Octal PSRAM, USB-OTG |
|
||||
| `CONFIG_IDF_TARGET_ESP32C3` | ESP32-C3 | RISC-V single-core | Minimal peripherals. RISC-V clamps out-of-range float→unsigned casts |
|
||||
| `CONFIG_IDF_TARGET_ESP32C5` | ESP32-C5 | RISC-V single-core | Wi-Fi 2.4Ghz + 5Ghz, Thread/Zigbee. Future target |
|
||||
| `CONFIG_IDF_TARGET_ESP32C6` | ESP32-C6 | RISC-V single-core | Wi-Fi 6, Thread/Zigbee. Future target |
|
||||
| `CONFIG_IDF_TARGET_ESP32P4` | ESP32-P4 | RISC-V dual-core | High performance. Future target |
|
||||
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
### Build-time validation
|
||||
WLED validates at compile time that exactly one target is defined and that it is a supported chip (`wled.cpp` lines 39–61). Follow this pattern when adding new chip-specific branches:
|
||||
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
```cpp
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32)
|
||||
// classic ESP32 path
|
||||
#elif defined(CONFIG_IDF_TARGET_ESP32S3)
|
||||
// S3-specific path
|
||||
#elif defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32C5) || defined(CONFIG_IDF_TARGET_ESP32C6) || defined(CONFIG_IDF_TARGET_ESP32P4)
|
||||
// RISC-V common path
|
||||
#else
|
||||
#warning "Untested chip — review peripheral availability"
|
||||
#endif
|
||||
```
|
||||
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
### Guidelines
|
||||
|
||||
- **Always test on the actual chip** before claiming support. Simulators and cross-compilation can hide peripheral differences.
|
||||
- **Prefer `#elif` chains** over nested `#ifdef` for readability.
|
||||
- **Do not use `CONFIG_IDF_TARGET_*` for feature detection.** Use `SOC_*` capability macros instead (see next section). For example, use `SOC_I2S_SUPPORTS_ADC` instead of `CONFIG_IDF_TARGET_ESP32` to check for I2S ADC support.
|
||||
- When a feature must be disabled on certain chips, use explicit `static_assert()` or `#warning` directives so the build clearly reports what is missing.
|
||||
|
||||
---
|
||||
|
||||
## Hardware Capability Detection: `SOC_*` Macros
|
||||
|
||||
`SOC_*` macros (from `soc/soc_caps.h`) describe what the current chip supports. They are the correct way to check for peripheral features — they stay accurate when new chips are added, unlike `CONFIG_IDF_TARGET_*` checks.
|
||||
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
### Important `SOC_*` macros used in WLED
|
||||
|
||||
| Macro | Type | Used in | Purpose |
|
||||
|---|---|---|---|
|
||||
| `SOC_I2S_NUM` | `int` | `audio_source.h` | Number of I2S peripherals (1 or 2) |
|
||||
| `SOC_I2S_SUPPORTS_ADC` | `bool` | `usermods/audioreactive/audio_source.h` | I2S ADC sampling mode (ESP32 only) |
|
||||
| `SOC_I2S_SUPPORTS_APLL` | `bool` | `usermods/audioreactive/audio_source.h` | Audio PLL for precise sample rates |
|
||||
| `SOC_I2S_SUPPORTS_PDM_RX` | `bool` | `usermods/audioreactive/audio_source.h` | PDM microphone input |
|
||||
| `SOC_ADC_MAX_BITWIDTH` | `int` | `util.cpp` | ADC resolution (12 or 13 bits). Renamed to `CONFIG_SOC_ADC_RTC_MAX_BITWIDTH` in IDF v5 |
|
||||
| `SOC_ADC_CHANNEL_NUM(unit)` | `int` | `pin_manager.cpp` | ADC channels per unit |
|
||||
| `SOC_UART_NUM` | `int` | `dmx_input.cpp` | Number of UART peripherals |
|
||||
| `SOC_DRAM_LOW` / `SOC_DRAM_HIGH` | `addr` | `util.cpp` | DRAM address boundaries for validation |
|
||||
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
### Key pitfall
|
||||
`SOC_ADC_MAX_BITWIDTH` (ADC resolution 12 or 13 bits) was renamed to `CONFIG_SOC_ADC_RTC_MAX_BITWIDTH` in IDF v5.
|
||||
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
### Less commonly used but valuable
|
||||
|
||||
| Macro | Purpose |
|
||||
|---|---|
|
||||
| `SOC_RMT_TX_CANDIDATES_PER_GROUP` | Number of RMT TX channels (varies 2–8 by chip) |
|
||||
| `SOC_LEDC_CHANNEL_NUM` | Number of LEDC (PWM) channels |
|
||||
| `SOC_GPIO_PIN_COUNT` | Total GPIO pin count |
|
||||
| `SOC_DAC_SUPPORTED` | Whether the chip has a DAC (ESP32/S2 only) |
|
||||
| `SOC_SPIRAM_SUPPORTED` | Whether PSRAM interface exists |
|
||||
| `SOC_CPU_CORES_NUM` | Core count (1 or 2) — useful for task pinning decisions |
|
||||
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
|
||||
### Best practices
|
||||
|
||||
```cpp
|
||||
// Good: feature-based detection
|
||||
#if SOC_I2S_SUPPORTS_PDM_RX
|
||||
_config.mode = i2s_mode_t(I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_PDM);
|
||||
#else
|
||||
#warning "PDM microphones not supported on this chip"
|
||||
#endif
|
||||
|
||||
// Avoid: chip-name-based detection
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32S3)
|
||||
// happens to be correct today, but breaks when a new chip adds PDM support
|
||||
#endif
|
||||
```
|
||||
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
### PSRAM capability macros
|
||||
|
||||
For PSRAM presence, mode, and DMA access patterns:
|
||||
|
||||
| Macro | Meaning |
|
||||
|---|---|
|
||||
| `CONFIG_SPIRAM` / `BOARD_HAS_PSRAM` | PSRAM is present in the build configuration |
|
||||
| `CONFIG_SPIRAM_MODE_QUAD` | Quad-SPI PSRAM (standard, used on ESP32 classic and some S2/S3 boards) |
|
||||
| `CONFIG_SPIRAM_MODE_OCT` | Octal-SPI PSRAM — 8 data lines, DTR mode. Used on ESP32-S3 with octal PSRAM (e.g. N8R8 / N16R8 modules). Reserves GPIO 33–37 for the PSRAM bus — **do not allocate these pins** when this macro is defined. `wled.cpp` uses this to gate GPIO reservation. |
|
||||
| `CONFIG_SPIRAM_MODE_HEX` | Hex-SPI (16-line) PSRAM — future interface on ESP32-P4 running at up to 200 MHz. Used in `json.cpp` to report the PSRAM mode. |
|
||||
| `CONFIG_SOC_PSRAM_DMA_CAPABLE` | PSRAM buffers can be used with DMA (ESP32-S3 with octal PSRAM) |
|
||||
| `CONFIG_SOC_MEMSPI_FLASH_PSRAM_INDEPENDENT` | SPI flash and PSRAM on separate buses (no speed contention) |
|
||||
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
|
||||
#### Detecting octal/hex flash
|
||||
|
||||
On ESP32-S3 modules with OPI flash (e.g. N8R8 modules where the SPI flash itself runs in Octal-PI mode), the build system sets:
|
||||
|
||||
| Macro | Meaning |
|
||||
|---|---|
|
||||
| `CONFIG_ESPTOOLPY_FLASHMODE_OPI` | Octal-PI flash mode. On S3, implies GPIO 33–37 are used by the flash/PSRAM interface — the same GPIO block as octal PSRAM. `wled.cpp` uses `CONFIG_ESPTOOLPY_FLASHMODE_OPI \|\| (CONFIG_SPIRAM_MODE_OCT && BOARD_HAS_PSRAM)` to decide whether to reserve these GPIOs. `json.cpp` uses this to report the flash mode string as `"🚀OPI"`. |
|
||||
| `CONFIG_ESPTOOLPY_FLASHMODE_HEX` | Hex flash mode (ESP32-P4). Reported as `"🚀🚀HEX"` in `json.cpp`. |
|
||||
|
||||
**Pattern used in WLED** (from `wled.cpp`) to reserve the octal-bus GPIOs on S3:
|
||||
```cpp
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32S3)
|
||||
#if CONFIG_ESPTOOLPY_FLASHMODE_OPI || (CONFIG_SPIRAM_MODE_OCT && defined(BOARD_HAS_PSRAM))
|
||||
// S3: GPIO 33-37 are used by the octal PSRAM/flash bus
|
||||
managed_pin_type pins[] = { {33, true}, {34, true}, {35, true}, {36, true}, {37, true} };
|
||||
pinManager.allocateMultiplePins(pins, sizeof(pins)/sizeof(managed_pin_type), PinOwner::SPI_RAM);
|
||||
#endif
|
||||
#endif
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ESP-IDF Version Conditionals
|
||||
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
### Checking the IDF version
|
||||
|
||||
```cpp
|
||||
#include <esp_idf_version.h>
|
||||
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)
|
||||
// IDF v5+ code path
|
||||
#elif ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0)
|
||||
// IDF v4.4+ code path
|
||||
#else
|
||||
// Legacy IDF v3/v4.x path
|
||||
#endif
|
||||
```
|
||||
|
||||
### Key ESP-IDF version thresholds for WLED
|
||||
|
||||
| Version | What changed |
|
||||
|---|---|
|
||||
| **4.0.0** | Filesystem API (`SPIFFS`/`LittleFS`), GPIO driver overhaul |
|
||||
| **4.2.0** | ADC/GPIO API updates; `esp_adc_cal` introduced |
|
||||
| **4.4.0** | I2S driver refactored (legacy API remains); `adc_deprecated.h` headers appear for newer targets |
|
||||
| **4.4.4–4.4.8** | Known I2S channel-swap regression on ESP32 (workaround in `audio_source.h`) |
|
||||
| **5.0.0** | **Major breaking changes** — RMT, I2S, ADC, SPI flash APIs replaced (see migration section) |
|
||||
| **5.1.0** | Matter protocol support; new `esp_flash` API stable |
|
||||
| **5.3+** | arduino-esp32 v3.x compatibility; C6/P4 support |
|
||||
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
### Guidelines
|
||||
|
||||
- When adding a version guard, **always include a comment** explaining *what* changed and *why* the guard is needed.
|
||||
- Avoid version ranges that silently break — prefer `>=` over exact version matches.
|
||||
- Known regressions should use explicit range guards:
|
||||
```cpp
|
||||
// IDF 4.4.4–4.4.8 swapped I2S left/right channels (fixed in 4.4.9)
|
||||
#if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 4)) && \
|
||||
(ESP_IDF_VERSION <= ESP_IDF_VERSION_VAL(4, 4, 8))
|
||||
#define I2S_CHANNELS_SWAPPED
|
||||
#endif
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Migrating from ESP-IDF v4.4.x to v5.x
|
||||
|
||||
The jump from IDF v4.4 (arduino-esp32 v2.x) to IDF v5.x (arduino-esp32 v3.x) is the largest API break in ESP-IDF history. This section documents the critical changes and recommended migration patterns based on the upstream WLED `V5-C6` branch (`https://github.com/wled/WLED/tree/V5-C6`). Note: WLED has not yet migrated to IDF v5 — these patterns prepare for the future migration.
|
||||
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
### Compiler changes
|
||||
|
||||
IDF v5.x ships a much newer GCC toolchain. Key versions:
|
||||
|
||||
| ESP-IDF | GCC | C++ default | Notes |
|
||||
|---|---|---|---|
|
||||
| 4.4.x (current) | **8.4.0** | C++17 (gnu++17) | Xtensa + RISC-V |
|
||||
| 5.1–5.3 | **13.2** | C++20 (gnu++2b) | Significant warning changes |
|
||||
| 5.4–5.5 | **14.2** | C++23 (gnu++2b) | Latest; stricter diagnostics |
|
||||
|
||||
Notable behavioral differences:
|
||||
|
||||
| Change | Impact | Action |
|
||||
|---|---|---|
|
||||
| Stricter `-Werror=enum-conversion` | Implicit int-to-enum casts now error | Use explicit `static_cast<>` or typed enums |
|
||||
| C++20/23 features available | `consteval`, `concepts`, `std::span`, `std::expected` | Use judiciously — ESP8266 builds still require GCC 10.x with C++17 |
|
||||
| `-Wdeprecated-declarations` enforced | Deprecated API calls become warnings/errors | Migrate to new APIs (see below) |
|
||||
| `-Wdangling-reference` (GCC 13+) | Warns when a reference binds to a temporary that will be destroyed | Fix the lifetime issue; do not suppress the warning |
|
||||
| `-fno-common` default (GCC 12+) | Duplicate tentative definitions across translation units cause linker errors | Use `extern` declarations in headers, define in exactly one `.cpp` |
|
||||
| RISC-V codegen improvements | C3/C6/P4 benefit from better register allocation | No action needed — automatic |
|
||||
|
||||
### C++ language features: GCC 8 → GCC 14
|
||||
|
||||
The jump from GCC 8.4 to GCC 14.2 spans six major compiler releases. This section lists features that become available and patterns that need updating.
|
||||
|
||||
#### Features safe to use after migration
|
||||
|
||||
These work in GCC 13+/14+ but **not** in GCC 8.4. Guard with `#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)` if the code must compile on both IDF v4 and v5.
|
||||
|
||||
| Feature | Standard | Example | Benefit |
|
||||
|---|---|---|---|
|
||||
| Designated initializers (C++20) | C++20 | `gpio_config_t cfg = { .mode = GPIO_MODE_OUTPUT };` | Already used as a GNU extension in GCC 8; becomes standard and portable in C++20 |
|
||||
| `[[likely]]` / `[[unlikely]]` | C++20 | `if (err != ESP_OK) [[unlikely]] { ... }` | Hints for branch prediction; useful in hot paths |
|
||||
| `[[nodiscard("reason")]]` | C++20 | `[[nodiscard("leak if ignored")]] void* allocBuffer();` | Enforces checking return values — helpful for `esp_err_t` wrappers |
|
||||
| `std::span<T>` | C++20 | `void process(std::span<uint8_t> buf)` | Safe, non-owning view of contiguous memory — replaces raw pointer + length pairs |
|
||||
| `consteval` | C++20 | `consteval uint32_t packColor(...)` | Guarantees compile-time evaluation; useful for color constants |
|
||||
| `constinit` | C++20 | `constinit static int counter = 0;` | Prevents static initialization order fiasco |
|
||||
| Concepts / `requires` | C++20 | `template<typename T> requires std::integral<T>` | Clearer constraints than SFINAE; improves error messages |
|
||||
| Three-way comparison (`<=>`) | C++20 | `auto operator<=>(const Version&) const = default;` | Less boilerplate for comparable types |
|
||||
| `std::bit_cast` | C++20 | `float f = std::bit_cast<float>(uint32_val);` | Type-safe reinterpretation — replaces `memcpy` or `union` tricks |
|
||||
| `if consteval` | C++23 | `if consteval { /* compile-time */ } else { /* runtime */ }` | Cleaner than `std::is_constant_evaluated()` |
|
||||
| `std::expected<T, E>` | C++23 | `std::expected<int, esp_err_t> readSensor()` | Monadic error handling — cleaner than returning error codes |
|
||||
| `std::to_underlying` | C++23 | `auto val = std::to_underlying(myEnum);` | Replaces `static_cast<int>(myEnum)` |
|
||||
|
||||
#### Features already available in GCC 8 (C++17)
|
||||
|
||||
These work on both IDF v4.4 and v5.x — prefer them now:
|
||||
|
||||
| Feature | Example | Notes |
|
||||
|---|---|---|
|
||||
| `if constexpr` | `if constexpr (sizeof(T) == 4) { ... }` | Compile-time branching; already used in WLED |
|
||||
| `std::optional<T>` | `std::optional<uint8_t> pin;` | Nullable value without sentinel values like `-1` |
|
||||
| `std::string_view` | `void log(std::string_view msg)` | Non-owning, non-allocating string reference |
|
||||
| Structured bindings | `auto [err, value] = readSensor();` | Useful with `std::pair` / `std::tuple` returns |
|
||||
| Fold expressions | `(addSegment(args), ...);` | Variadic template expansion |
|
||||
| Inline variables | `inline constexpr int MAX_PINS = 50;` | Avoids ODR issues with header-defined constants |
|
||||
| `[[maybe_unused]]` | `[[maybe_unused]] int debug_only = 0;` | Suppresses unused-variable warnings cleanly |
|
||||
| `[[fallthrough]]` | `case 1: doA(); [[fallthrough]]; case 2:` | Documents intentional switch fallthrough |
|
||||
| Nested namespaces | `namespace wled::audio { }` | Shorter than nested `namespace` blocks |
|
||||
|
||||
#### Patterns that break or change behavior
|
||||
|
||||
| Pattern | GCC 8 behavior | GCC 14 behavior | Fix |
|
||||
|---|---|---|---|
|
||||
| `int x; enum E e = x;` | Warning (often ignored) | Error with `-Werror=enum-conversion` | `E e = static_cast<E>(x);` |
|
||||
| `int g;` in two `.cpp` files | Both compile, linker merges (tentative definition) | Error: multiple definitions (`-fno-common`) | `extern int g;` in header, `int g;` in one `.cpp` |
|
||||
| `const char* ref = std::string(...).c_str();` | Silent dangling pointer | Warning (`-Wdangling-reference`) | Extend lifetime: store the `std::string` in a local variable |
|
||||
| `register int x;` | Accepted (ignored) | Warning or error (`register` removed in C++17) | Remove `register` keyword |
|
||||
| Narrowing in aggregate init | Warning | Error | Use explicit cast or wider type |
|
||||
| Implicit `this` capture in lambdas | Accepted in `[=]` | Deprecated warning; error in C++20 mode | Use `[=, this]` or `[&]` |
|
||||
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
#### Recommendations
|
||||
|
||||
- **Do not raise the minimum C++ standard yet.** WLED must still build on IDF v4.4 (GCC 8.4, C++17). Use `#if __cplusplus > 201703L` to gate C++20 features.
|
||||
- **Mark intentional fallthrough** with `[[fallthrough]]` — GCC 14 warns on unmarked fallthrough by default.
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
- **Prefer `std::optional` over sentinel values** (e.g., `-1` for "no pin") in new code — it works on both compilers.
|
||||
- **Use `std::string_view`** for read-only string parameters instead of `const char*` or `const String&` — zero-copy and works on GCC 8+.
|
||||
- **Avoid raw `union` type punning** — prefer `memcpy` (GCC 8) or `std::bit_cast` (GCC 13+) for strict-aliasing safety.
|
||||
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
### Deprecated and removed APIs
|
||||
|
||||
#### RMT (Remote Control Transceiver)
|
||||
|
||||
The legacy `rmt_*` functions are removed in IDF v5. Do not introduce new legacy RMT calls.
|
||||
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
The new API is channel-based:
|
||||
|
||||
| IDF v4 (legacy) | IDF v5 (new) | Notes |
|
||||
|---|---|---|
|
||||
| `rmt_config()` + `rmt_driver_install()` | `rmt_new_tx_channel()` / `rmt_new_rx_channel()` | Channels are now objects |
|
||||
| `rmt_write_items()` | `rmt_transmit()` with encoder | Requires `rmt_encoder_t` |
|
||||
| `rmt_set_idle_level()` | Configure in channel config | Set at creation time |
|
||||
| `rmt_item32_t` | `rmt_symbol_word_t` | Different struct layout |
|
||||
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
**WLED impact**: NeoPixelBus LED output and IR receiver both use legacy RMT. The upstream `V5-C6` branch adds `-D WLED_USE_SHARED_RMT` and disables IR until the library is ported.
|
||||
|
||||
#### I2S (Inter-IC Sound)
|
||||
|
||||
Legacy `i2s_driver_install()` + `i2s_read()` API is deprecated. When touching audio source code, wrap legacy I2S init and reading in `#if ESP_IDF_VERSION_MAJOR < 5` / `#else`.
|
||||
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
The new API uses channel handles:
|
||||
| IDF v4 (legacy) | IDF v5 (new) | Notes |
|
||||
|---|---|---|
|
||||
| `i2s_driver_install()` | `i2s_channel_init_std_mode()` | Separate STD/PDM/TDM modes |
|
||||
| `i2s_set_pin()` | Pin config in `i2s_std_gpio_config_t` | Set at init time |
|
||||
| `i2s_read()` | `i2s_channel_read()` | Uses channel handle |
|
||||
| `i2s_set_clk()` | `i2s_channel_reconfig_std_clk()` | Reconfigure running channel |
|
||||
| `i2s_config_t` | `i2s_std_config_t` | Separate config for each mode |
|
||||
|
||||
**Migration pattern** (from Espressif examples):
|
||||
```cpp
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)
|
||||
#include "driver/i2s_std.h"
|
||||
i2s_chan_handle_t rx_handle;
|
||||
i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_0, I2S_ROLE_MASTER);
|
||||
i2s_new_channel(&chan_cfg, NULL, &rx_handle);
|
||||
|
||||
i2s_std_config_t std_cfg = {
|
||||
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(22050),
|
||||
.slot_cfg = I2S_STD_MSB_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_32BIT, I2S_SLOT_MODE_MONO),
|
||||
.gpio_cfg = { .din = GPIO_NUM_32, .mclk = I2S_GPIO_UNUSED, ... },
|
||||
};
|
||||
i2s_channel_init_std_mode(rx_handle, &std_cfg);
|
||||
i2s_channel_enable(rx_handle);
|
||||
#else
|
||||
// Legacy i2s_driver_install() path
|
||||
#endif
|
||||
```
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
**WLED impact**: The audioreactive usermod (`audio_source.h`) heavily uses legacy I2S. Migration requires rewriting the `I2SSource` class for channel-based API.
|
||||
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
#### ADC (Analog-to-Digital Converter)
|
||||
|
||||
Legacy `adc1_get_raw()` and `esp_adc_cal_*` are deprecated:
|
||||
|
||||
| IDF v4 (legacy) | IDF v5 (new) | Notes |
|
||||
|---|---|---|
|
||||
| `adc1_config_width()` + `adc1_get_raw()` | `adc_oneshot_new_unit()` + `adc_oneshot_read()` | Object-based API |
|
||||
| `esp_adc_cal_characterize()` | `adc_cali_create_scheme_*()` | Calibration is now scheme-based |
|
||||
| `adc_continuous_*` (old) | `adc_continuous_*` (restructured) | Config struct changes |
|
||||
|
||||
#### SPI Flash
|
||||
|
||||
| IDF v4 (legacy) | IDF v5 (new) |
|
||||
|---|---|
|
||||
| `spi_flash_read()` | `esp_flash_read()` |
|
||||
| `spi_flash_write()` | `esp_flash_write()` |
|
||||
| `spi_flash_erase_range()` | `esp_flash_erase_region()` |
|
||||
|
||||
WLED already has a compatibility shim in `ota_update.cpp` that maps old names to new ones.
|
||||
|
||||
#### GPIO
|
||||
|
||||
| IDF v4 (legacy) | IDF v5 (recommended) |
|
||||
|---|---|
|
||||
| `gpio_pad_select_gpio()` | `esp_rom_gpio_pad_select_gpio()` (or use `gpio_config()`) |
|
||||
| `gpio_set_direction()` + `gpio_set_pull_mode()` | `gpio_config()` with `gpio_config_t` struct |
|
||||
|
||||
### Features disabled in IDF v5 builds
|
||||
|
||||
The upstream `V5-C6` branch explicitly disables features with incompatible library dependencies:
|
||||
|
||||
```ini
|
||||
# platformio.ini [esp32_idf_V5]
|
||||
-D WLED_DISABLE_INFRARED # IR library uses legacy RMT
|
||||
-D WLED_DISABLE_MQTT # AsyncMqttClient incompatible with IDF v5
|
||||
-D ESP32_ARDUINO_NO_RGB_BUILTIN # Prevents RMT driver conflict with built-in LED
|
||||
-D WLED_USE_SHARED_RMT # Use new shared RMT driver for NeoPixel output
|
||||
```
|
||||
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
### Migration checklist for new code
|
||||
|
||||
1. **Never use a removed API without a version guard.** Always provide both old and new paths, or disable the feature on IDF v5.
|
||||
2. **Test on both IDF v4.4 and v5.x builds** if the code must be backward-compatible.
|
||||
3. **Prefer the newer API** when writing new code — wrap the old API in an `#else` block.
|
||||
4. **Mark migration TODOs** with `// TODO(idf5):` so they are easy to find later.
|
||||
|
||||
---
|
||||
|
||||
## Memory Management: `heap_caps_*` Best Practices
|
||||
|
||||
ESP32 has multiple memory regions with different capabilities. Using the right allocator is critical for performance and stability.
|
||||
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
### Memory regions
|
||||
|
||||
| Region | Flag | Speed | DMA | Size | Use for |
|
||||
|---|---|---|---|---|---|
|
||||
| DRAM | `MALLOC_CAP_INTERNAL \| MALLOC_CAP_8BIT` | Fast | Yes (ESP32) | 200–320 KB | Hot-path buffers, task stacks, small allocations |
|
||||
| IRAM | `MALLOC_CAP_EXEC` | Fastest | No | 32–128 KB | Code (automatic via `IRAM_ATTR`) |
|
||||
| PSRAM (SPIRAM) | `MALLOC_CAP_SPIRAM \| MALLOC_CAP_8BIT` | Slower | Chip-dependent | 2–16 MB | Large buffers, JSON documents, image data |
|
||||
| RTC RAM | `MALLOC_CAP_RTCRAM` | Moderate | No | 8 KB | Data surviving deep sleep; small persistent buffers |
|
||||
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
|
||||
### WLED allocation wrappers
|
||||
|
||||
WLED provides convenience wrappers with automatic fallback. **Always prefer these over raw `heap_caps_*` calls**:
|
||||
|
||||
| Function | Allocation preference | Use case |
|
||||
|---|---|---|
|
||||
| `d_malloc(size)` | RTC → DRAM → PSRAM | General-purpose; prefers fast memory |
|
||||
| `d_calloc(n, size)` | Same as `d_malloc`, zero-initialized | Arrays, structs |
|
||||
| `p_malloc(size)` | PSRAM → DRAM | Large buffers; prefers abundant memory |
|
||||
| `p_calloc(n, size)` | Same as `p_malloc`, zero-initialized | Large arrays |
|
||||
| `d_malloc_only(size)` | RTC → DRAM (no PSRAM fallback) | DMA buffers, time-critical data |
|
||||
|
||||
### PSRAM guidelines
|
||||
|
||||
- **Check availability**: always test `psramFound()` before assuming PSRAM is present.
|
||||
- **DMA compatibility**: on ESP32 (classic), PSRAM buffers are **not DMA-capable** — use `d_malloc_only()` to allocate DMA buffers in DRAM only. On ESP32-S3 with octal PSRAM (`CONFIG_SPIRAM_MODE_OCT`), PSRAM buffers *can* be used with DMA when `CONFIG_SOC_PSRAM_DMA_CAPABLE` is defined.
|
||||
- **JSON documents**: use the `PSRAMDynamicJsonDocument` allocator (defined in `wled.h`) to put large JSON documents in PSRAM:
|
||||
```cpp
|
||||
PSRAMDynamicJsonDocument doc(16384); // allocated in PSRAM if available
|
||||
```
|
||||
- **Fragmentation**: PSRAM allocations fragment less than DRAM because the region is larger. But avoid mixing small and large allocations in PSRAM — small allocations waste the MMU page granularity.
|
||||
- **Heap validation**: use `d_measureHeap()` and `d_measureContiguousFreeHeap()` to monitor remaining DRAM. Allocations that would drop free DRAM below `MIN_HEAP_SIZE` should go to PSRAM instead.
|
||||
- **Performance**: Keep hot-path data in DRAM. Prefer PSRAM for capacity-oriented buffers and monitor contiguous DRAM headroom.
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
PSRAM access is up to 15× slower than DRAM on ESP32, 3–10× slower than DRAM on ESP32-S3/-S2 with quad-SPI bus. On ESP32-S3 with octal PSRAM (`CONFIG_SPIRAM_MODE_OCT`), the penalty is smaller (~2×) because the 8-line DTR bus can transfer 8 bits in parallel at 80 MHz (120 MHz is possible with CONFIG_SPIRAM_SPEED_120M, which requires enabling experimental ESP-IDF features). On ESP32-P4 with hex PSRAM (`CONFIG_SPIRAM_MODE_HEX`), the 16-line bus runs at 200 MHz which brings it on-par with DRAM. Keep hot-path data in DRAM regardless, but consider that ESP32 often crashes when the largest DRAM chunk gets below 10 KB.
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
### Pattern: preference-based allocation
|
||||
|
||||
When you need a buffer that works on boards with or without PSRAM:
|
||||
|
||||
```cpp
|
||||
// Prefer PSRAM for large buffers, fall back to DRAM
|
||||
uint8_t* buf = (uint8_t*)heap_caps_malloc_prefer(bufSize, 2,
|
||||
MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT, // first choice: PSRAM
|
||||
MALLOC_CAP_DEFAULT); // fallback: any available
|
||||
// Or simply:
|
||||
uint8_t* buf = (uint8_t*)p_malloc(bufSize);
|
||||
```
|
||||
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
---
|
||||
|
||||
## I2S Audio: Best Practices
|
||||
|
||||
The audioreactive usermod uses I2S for microphone input. Key patterns:
|
||||
|
||||
### Port selection
|
||||
|
||||
```cpp
|
||||
constexpr i2s_port_t AR_I2S_PORT = I2S_NUM_0;
|
||||
// I2S_NUM_1 has limitations: no MCLK routing, no ADC support, no PDM support
|
||||
```
|
||||
|
||||
Always use `I2S_NUM_0` unless you have a specific reason and have verified support on all target chips.
|
||||
|
||||
### DMA buffer tuning
|
||||
|
||||
DMA buffer size controls latency vs. reliability:
|
||||
|
||||
| Scenario | `dma_buf_count` | `dma_buf_len` | Latency | Notes |
|
||||
|---|---|---|---|---|
|
||||
| With HUB75 matrix | 18 | 128 | ~100 ms | Higher count prevents I2S starvation during matrix DMA |
|
||||
| Without PSRAM | 24 | 128 | ~140 ms | More buffers compensate for slower interrupt response |
|
||||
| Default | 8 | 128 | ~46 ms | Acceptable for most setups |
|
||||
|
||||
### Interrupt priority
|
||||
|
||||
Choose interrupt priority based on coexistence with other drivers:
|
||||
|
||||
```cpp
|
||||
#ifdef WLED_ENABLE_HUB75MATRIX
|
||||
.intr_alloc_flags = ESP_INTR_FLAG_IRAM | ESP_INTR_FLAG_LEVEL1, // level 1 (lowest) to avoid starving HUB75
|
||||
#else
|
||||
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL2 | ESP_INTR_FLAG_LEVEL3, // accept level 2 or 3 (allocator picks available)
|
||||
#endif
|
||||
```
|
||||
|
||||
### APLL (Audio PLL) usage
|
||||
|
||||
The ESP32 has an audio PLL for precise sample rates. Rules:
|
||||
|
||||
- Enable APLL when an MCLK pin is provided and precision matters.
|
||||
- **Disable APLL** when Ethernet or HUB75 is active — they also use the APLL.
|
||||
- APLL is broken on ESP32 revision 0 silicon.
|
||||
- Not all chips have APLL — gate with `SOC_I2S_SUPPORTS_APLL`.
|
||||
|
||||
```cpp
|
||||
#if !defined(SOC_I2S_SUPPORTS_APLL)
|
||||
_config.use_apll = false;
|
||||
#elif defined(WLED_USE_ETHERNET) || defined(WLED_ENABLE_HUB75MATRIX)
|
||||
_config.use_apll = false; // APLL conflict
|
||||
#endif
|
||||
```
|
||||
|
||||
### PDM microphone caveats
|
||||
|
||||
- Not supported on ESP32-C3 (`SOC_I2S_SUPPORTS_PDM_RX` not defined).
|
||||
- ESP32-S3 PDM has known issues: sample rate at 50% of expected, very low amplitude.
|
||||
- **16-bit data width**: Espressif's IDF documentation states that in PDM mode the data unit width is always 16 bits, regardless of the configured `bits_per_sample`.
|
||||
- See [espressif/esp-idf#8660](https://github.com/espressif/esp-idf/issues/8660) for the upstream issue.
|
||||
- **Flag `bits_per_sample = I2S_BITS_PER_SAMPLE_32BIT` in PDM mode** — this causes the S3 low-amplitude symptom.
|
||||
- No clock pin (`I2S_CKPIN = -1`) triggers PDM mode in WLED.
|
||||
|
||||
---
|
||||
|
||||
## HUB75 LED Matrix: Best Practices
|
||||
|
||||
WLED uses the `ESP32-HUB75-MatrixPanel-I2S-DMA` library for HUB75 matrix output.
|
||||
|
||||
### Chip-specific panel limits
|
||||
|
||||
```cpp
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32S3) && defined(BOARD_HAS_PSRAM)
|
||||
maxChainLength = 6; // S3 + PSRAM: up to 6 panels (DMA-capable PSRAM)
|
||||
#elif defined(CONFIG_IDF_TARGET_ESP32S2)
|
||||
maxChainLength = 2; // S2: limited DMA channels
|
||||
#else
|
||||
maxChainLength = 4; // Classic ESP32: default
|
||||
#endif
|
||||
```
|
||||
|
||||
### Color depth vs. pixel count
|
||||
|
||||
The driver dynamically reduces color depth for larger displays to stay within DMA buffer limits:
|
||||
|
||||
| Pixel count | Color depth | Bits per pixel |
|
||||
|---|---|---|
|
||||
| ≤ `MAX_PIXELS_10BIT` | 10-bit (30-bit color) | High quality (experimental) |
|
||||
| ≤ `MAX_PIXELS_8BIT` | 8-bit (24-bit color) | Full quality |
|
||||
| ≤ `MAX_PIXELS_6BIT` | 6-bit (18-bit color) | Slight banding |
|
||||
| ≤ `MAX_PIXELS_4BIT` | 4-bit (12-bit color) | Visible banding |
|
||||
| larger | 3-bit (9-bit color) | Minimal color range |
|
||||
|
||||
### Resource conflicts
|
||||
|
||||
- **APLL**: HUB75 I2S DMA uses the APLL. Disable APLL in the audio I2S driver when HUB75 is active.
|
||||
- **I2S peripheral**: HUB75 uses `I2S_NUM_1` (or `I2S_NUM_0` on single-I2S chips). Audio must use the other port.
|
||||
- **Pin count**: HUB75 requires 13–14 GPIO pins. On ESP32-S2 this severely limits remaining GPIO.
|
||||
- **Reboot required**: on ESP32-S3, changing HUB75 driver options requires a full reboot — the I2S DMA cannot be reconfigured at runtime.
|
||||
|
||||
---
|
||||
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
## GPIO Best Practices
|
||||
|
||||
### Prefer `gpio_config()` over individual calls
|
||||
|
||||
```cpp
|
||||
// Preferred: single struct-based configuration
|
||||
gpio_config_t io_conf = {
|
||||
.pin_bit_mask = (1ULL << pin),
|
||||
.mode = GPIO_MODE_OUTPUT,
|
||||
.pull_up_en = GPIO_PULLUP_DISABLE,
|
||||
.pull_down_en = GPIO_PULLDOWN_DISABLE,
|
||||
.intr_type = GPIO_INTR_DISABLE,
|
||||
};
|
||||
gpio_config(&io_conf);
|
||||
|
||||
// Avoid: multiple separate calls (more error-prone, deprecated in IDF v5)
|
||||
gpio_set_direction(pin, GPIO_MODE_OUTPUT);
|
||||
gpio_set_pull_mode(pin, GPIO_FLOATING);
|
||||
```
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
|
||||
### Pin manager integration
|
||||
|
||||
Always allocate pins through WLED's `pinManager` before using GPIO APIs:
|
||||
|
||||
```cpp
|
||||
if (!pinManager.allocatePin(myPin, true, PinOwner::UM_MyUsermod)) {
|
||||
return; // pin in use by another module
|
||||
}
|
||||
// Now safe to configure
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Timer Best Practices
|
||||
|
||||
### Microsecond timing
|
||||
|
||||
For high-resolution timing, prefer `esp_timer_get_time()` (microsecond resolution, 64-bit) over `millis()` or `micros()`.
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
|
||||
```cpp
|
||||
#include <esp_timer.h>
|
||||
int64_t now_us = esp_timer_get_time(); // monotonic, not affected by NTP
|
||||
```
|
||||
|
||||
> **Note**: In arduino-esp32, both `millis()` and `micros()` are thin wrappers around `esp_timer_get_time()` — they share the same monotonic clock source. Prefer the direct call when you need the full 64-bit value or ISR-safe access without truncation:
|
||||
> ```cpp
|
||||
> // arduino-esp32 internals (cores/esp32/esp32-hal-misc.c):
|
||||
> // unsigned long micros() { return (unsigned long)(esp_timer_get_time()); }
|
||||
> // unsigned long millis() { return (unsigned long)(esp_timer_get_time() / 1000ULL); }
|
||||
> ```
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
### Periodic timers
|
||||
|
||||
For periodic tasks with sub-millisecond precision, use `esp_timer`:
|
||||
|
||||
```cpp
|
||||
esp_timer_handle_t timer;
|
||||
esp_timer_create_args_t args = {
|
||||
.callback = myCallback,
|
||||
.arg = nullptr,
|
||||
.dispatch_method = ESP_TIMER_TASK, // run in timer task (not ISR)
|
||||
.name = "my_timer",
|
||||
};
|
||||
esp_timer_create(&args, &timer);
|
||||
esp_timer_start_periodic(timer, 1000); // 1 ms period
|
||||
```
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
|
||||
Always prefer `ESP_TIMER_TASK` dispatch over `ESP_TIMER_ISR` unless you need ISR-level latency — ISR callbacks have severe restrictions (no logging, no heap allocation, no FreeRTOS API calls).
|
||||
|
||||
### Precision waiting: coarse delay then spin-poll
|
||||
|
||||
When waiting for a precise future deadline (e.g., FPS limiting, protocol timing), avoid spinning the entire duration — that wastes CPU and starves other tasks. Instead, yield to FreeRTOS while time allows, then spin only for the final window.
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
```cpp
|
||||
// Wait until 'target_us' (a micros() / esp_timer_get_time() timestamp)
|
||||
long time_to_wait = (long)(target_us - micros());
|
||||
// Coarse phase: yield to FreeRTOS while we have more than ~2 ms remaining.
|
||||
// vTaskDelay(1) suspends the task for one RTOS tick, letting other task run freely.
|
||||
while (time_to_wait > 2000) {
|
||||
vTaskDelay(1);
|
||||
time_to_wait = (long)(target_us - micros());
|
||||
}
|
||||
// Fine phase: busy-poll the last ≤2 ms for microsecond accuracy.
|
||||
// micros() wraps esp_timer_get_time() so this is low-overhead.
|
||||
while ((long)(target_us - micros()) > 0) { /* spin */ }
|
||||
```
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
|
||||
> The threshold (2000 µs as an example) should be at least one RTOS tick (default 1 ms on ESP32) plus some margin. A value of 1500–3000 µs works well in practice.
|
||||
|
||||
---
|
||||
|
||||
## ADC Best Practices
|
||||
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
### Version-aware ADC code
|
||||
|
||||
ADC is one of the most fragmented APIs across IDF versions:
|
||||
|
||||
```cpp
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)
|
||||
// IDF v5: oneshot driver
|
||||
#include "esp_adc/adc_oneshot.h"
|
||||
#include "esp_adc/adc_cali.h"
|
||||
adc_oneshot_unit_handle_t adc_handle;
|
||||
adc_oneshot_unit_init_cfg_t unit_cfg = { .unit_id = ADC_UNIT_1 };
|
||||
adc_oneshot_new_unit(&unit_cfg, &adc_handle);
|
||||
#else
|
||||
// IDF v4: legacy driver
|
||||
#include "driver/adc.h"
|
||||
#include "esp_adc_cal.h"
|
||||
adc1_config_width(ADC_WIDTH_BIT_12);
|
||||
int raw = adc1_get_raw(ADC1_CHANNEL_0);
|
||||
#endif
|
||||
```
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
|
||||
### Bit width portability
|
||||
|
||||
Not all chips have 12-bit ADC. `SOC_ADC_MAX_BITWIDTH` reports the maximum resolution (12 or 13 bits). Note that in IDF v5, this macro was renamed to `CONFIG_SOC_ADC_RTC_MAX_BITWIDTH`. Write version-aware guards:
|
||||
|
||||
```cpp
|
||||
// IDF v4: SOC_ADC_MAX_BITWIDTH IDF v5: CONFIG_SOC_ADC_RTC_MAX_BITWIDTH
|
||||
#if defined(CONFIG_SOC_ADC_RTC_MAX_BITWIDTH) // IDF v5+
|
||||
#define MY_ADC_MAX_BITWIDTH CONFIG_SOC_ADC_RTC_MAX_BITWIDTH
|
||||
#elif defined(SOC_ADC_MAX_BITWIDTH) // IDF v4
|
||||
#define MY_ADC_MAX_BITWIDTH SOC_ADC_MAX_BITWIDTH
|
||||
#else
|
||||
#define MY_ADC_MAX_BITWIDTH 12 // safe fallback
|
||||
#endif
|
||||
|
||||
#if MY_ADC_MAX_BITWIDTH == 13
|
||||
adc1_config_width(ADC_WIDTH_BIT_13); // ESP32-S2
|
||||
#else
|
||||
adc1_config_width(ADC_WIDTH_BIT_12); // ESP32, S3, C3, etc.
|
||||
#endif
|
||||
```
|
||||
|
||||
WLED's `util.cpp` uses the IDF v4 form (`SOC_ADC_MAX_BITWIDTH`) — this will need updating when the codebase migrates to IDF v5.
|
||||
|
||||
---
|
||||
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
## RMT Best Practices
|
||||
|
||||
### Current usage in WLED
|
||||
|
||||
RMT drives NeoPixel LED output (via NeoPixelBus) and IR receiver input. Both use the legacy API that is removed in IDF v5.
|
||||
|
||||
### Migration notes
|
||||
|
||||
- The upstream `V5-C6` branch uses `-D WLED_USE_SHARED_RMT` to switch to the new RMT driver for NeoPixel output.
|
||||
- IR is disabled on IDF v5 until the IR library is ported.
|
||||
- New chips (C6, P4) have different RMT channel counts — use `SOC_RMT_TX_CANDIDATES_PER_GROUP` to check availability.
|
||||
- The new RMT API requires an "encoder" object (`rmt_encoder_t`) to translate data formats — this is more flexible but requires more setup code.
|
||||
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
---
|
||||
|
||||
## Espressif Best Practices (from official examples)
|
||||
|
||||
### Error handling
|
||||
|
||||
Always check `esp_err_t` return values. Use `ESP_ERROR_CHECK()` in initialization code, but handle errors gracefully in runtime code.
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
```cpp
|
||||
// Initialization — crash early on failure
|
||||
ESP_ERROR_CHECK(i2s_driver_install(I2S_NUM_0, &config, 0, nullptr));
|
||||
|
||||
// Runtime — log and recover
|
||||
esp_err_t err = i2s_read(I2S_NUM_0, buf, len, &bytes_read, portMAX_DELAY);
|
||||
if (err != ESP_OK) {
|
||||
DEBUGSR_PRINTF("I2S read failed: %s\n", esp_err_to_name(err));
|
||||
return;
|
||||
}
|
||||
```
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
|
||||
For situations between these two extremes — where you want the `ESP_ERROR_CHECK` formatted log message (file, line, error name) but must not abort — use `ESP_ERROR_CHECK_WITHOUT_ABORT()`.
|
||||
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
```cpp
|
||||
// Logs in the same format as ESP_ERROR_CHECK, but returns the error code instead of aborting.
|
||||
// Useful for non-fatal driver calls where you want visibility without crashing.
|
||||
esp_err_t err = ESP_ERROR_CHECK_WITHOUT_ABORT(i2s_set_clk(AR_I2S_PORT, rate, bits, ch));
|
||||
if (err != ESP_OK) return; // handle as needed
|
||||
```
|
||||
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
### Logging
|
||||
|
||||
WLED uses its own logging macros — **not** `ESP_LOGx()`. For application-level code, always use the WLED macros defined in `wled.h`:
|
||||
|
||||
| Macro family | Defined in | Controlled by | Use for |
|
||||
|---|---|---|---|
|
||||
| `DEBUG_PRINT` / `DEBUG_PRINTLN` / `DEBUG_PRINTF` | `wled.h` | `WLED_DEBUG` build flag | Development/diagnostic output; compiled out in release builds |
|
||||
|
||||
All of these wrap `Serial` output through the `DEBUGOUT` / `DEBUGOUTLN` / `DEBUGOUTF` macros.
|
||||
|
||||
**Exception — low-level driver code**: When writing code that interacts directly with ESP-IDF APIs (e.g., I2S initialization, RMT setup), use `ESP_LOGx()` macros instead. They support tag-based filtering and compile-time log level control:
|
||||
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
```cpp
|
||||
static const char* TAG = "my_module";
|
||||
ESP_LOGI(TAG, "Initialized with %d buffers", count);
|
||||
ESP_LOGW(TAG, "PSRAM not available, falling back to DRAM");
|
||||
ESP_LOGE(TAG, "Failed to allocate %u bytes", size);
|
||||
```
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
### Task creation and pinning
|
||||
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
On dual-core chips (ESP32, S3, P4), pin latency-sensitive tasks to a specific core:
|
||||
|
||||
```cpp
|
||||
xTaskCreatePinnedToCore(
|
||||
audioTask, // function
|
||||
"audio", // name
|
||||
4096, // stack size
|
||||
nullptr, // parameter
|
||||
5, // priority (higher = more important)
|
||||
&audioTaskHandle, // handle
|
||||
0 // core ID (0 = protocol core, 1 = app core)
|
||||
);
|
||||
```
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
|
||||
Guidelines:
|
||||
- Pin network/protocol tasks to core 0 (where Wi-Fi runs).
|
||||
- Pin real-time tasks (audio, LED output) to core 1.
|
||||
- On single-core chips (S2, C3, C5, C6), only core 0 exists — pinning to core 1 will fail. Use `SOC_CPU_CORES_NUM > 1` guards or `tskNO_AFFINITY`.
|
||||
- Use `SOC_CPU_CORES_NUM` to conditionally pin tasks:
|
||||
```cpp
|
||||
#if SOC_CPU_CORES_NUM > 1
|
||||
xTaskCreatePinnedToCore(audioTask, "audio", 4096, nullptr, 5, &handle, 1);
|
||||
#else
|
||||
xTaskCreate(audioTask, "audio", 4096, nullptr, 5, &handle);
|
||||
#endif
|
||||
```
|
||||
|
||||
**Tip: use xTaskCreateUniversal()** - from arduino-esp32 - to avoid the conditional on `SOC_CPU_CORES_NUM`. This function has the same signature as ``xTaskCreatePinnedToCore()``, but automatically falls back to ``xTaskCreate()`` on single-core MCUs.
|
||||
|
||||
### `delay()`, `yield()`, and the IDLE task
|
||||
|
||||
FreeRTOS on ESP32 is **preemptive** — all tasks are scheduled by priority regardless of `yield()` calls. This is fundamentally different from ESP8266 cooperative multitasking.
|
||||
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
| Call | What it does | Reaches IDLE (priority 0)? |
|
||||
|---|---|---|
|
||||
| `delay(ms)` / `vTaskDelay(ticks)` | Suspends calling task; scheduler runs all other ready tasks | ✅ Yes |
|
||||
| `yield()` / `vTaskDelay(0)` | Hint to switch to tasks at **equal or higher** priority only | ❌ No |
|
||||
| `taskYIELD()` | Same as `vTaskDelay(0)` | ❌ No |
|
||||
| Blocking API (`xQueueReceive`, `ulTaskNotifyTake`, `vTaskDelayUntil`) | Suspends task until event or timeout; IDLE runs freely | ✅ Yes |
|
||||
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
|
||||
**`delay()` in `loopTask` is safe.** Arduino's `loop()` runs inside `loopTask`. Calling `delay()` suspends only `loopTask` — all other FreeRTOS tasks (Wi-Fi stack, audio FFT, LED DMA) continue uninterrupted on either core.
|
||||
|
||||
**`yield()` does not yield to IDLE.** Any task that loops with only `yield()` calls will starve the IDLE task, causing the IDLE watchdog to fire. Always use `delay(1)` (or a blocking FreeRTOS call) in tight task loops. Note: WLED redefines `yield()` as an empty macro on ESP32 WLEDMM_FASTPATH builds.
|
||||
|
||||
#### Why the IDLE task is not optional
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
|
||||
The FreeRTOS IDLE task (one per core on dual-core ESP32 and ESP32-S3; single instance on single-core chips) is not idle in the casual sense — it performs essential system housekeeping:
|
||||
|
||||
- **Frees deleted task memory**: when a task calls `vTaskDelete()`, the IDLE task reclaims its TCB and stack. Without IDLE running, deleted tasks leak memory permanently.
|
||||
- **Runs the idle hook**: when `configUSE_IDLE_HOOK = 1`, the IDLE task calls `vApplicationIdleHook()` on every iteration — some ESP-IDF components register low-priority background work here.
|
||||
- **Implements tickless idle / light sleep**: on battery-powered devices, IDLE is the entry point for low-power sleep. A permanently starved IDLE task disables light sleep entirely.
|
||||
- **Runs registered idle hooks**: ESP-IDF components register callbacks via `esp_register_freertos_idle_hook()` (e.g., Wi-Fi background maintenance, Bluetooth housekeeping). These only fire when IDLE runs.
|
||||
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
|
||||
In short: **starving IDLE corrupts memory cleanup, breaks background activities, disables low-power sleep, and prevents Wi-Fi/BT maintenance.** The IDLE watchdog panic is a symptom — the real damage happens before the watchdog fires.
|
||||
|
||||
### Watchdog management
|
||||
|
||||
Long-running operations may trigger the task watchdog. Feed it explicitly:
|
||||
|
||||
```cpp
|
||||
#include <esp_task_wdt.h>
|
||||
esp_task_wdt_reset(); // feed the watchdog in long loops
|
||||
```
|
||||
|
||||
For tasks that intentionally block for extended periods, consider subscribing/unsubscribing from the TWDT:
|
||||
|
||||
```cpp
|
||||
esp_task_wdt_delete(NULL); // remove current task from TWDT (IDF v4.4)
|
||||
// ... long blocking operation ...
|
||||
esp_task_wdt_add(NULL); // re-register
|
||||
```
|
||||
|
||||
> **IDF v5 note**: In IDF v5, `esp_task_wdt_add()` and `esp_task_wdt_delete()` require an explicit `TaskHandle_t`. Use `xTaskGetCurrentTaskHandle()` instead of `NULL`.
|
||||
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
---
|
||||
|
||||
## Quick Reference: IDF v4 → v5 API Mapping
|
||||
|
||||
| Component | IDF v4 Header | IDF v5 Header | Key Change |
|
||||
|---|---|---|---|
|
||||
| I2S | `driver/i2s.h` | `driver/i2s_std.h` | Channel-based API |
|
||||
| ADC (oneshot) | `driver/adc.h` | `esp_adc/adc_oneshot.h` | Unit/channel handles |
|
||||
| ADC (calibration) | `esp_adc_cal.h` | `esp_adc/adc_cali.h` | Scheme-based calibration |
|
||||
| RMT | `driver/rmt.h` | `driver/rmt_tx.h` / `rmt_rx.h` | Encoder-based transmit |
|
||||
| SPI Flash | `spi_flash.h` | `esp_flash.h` | `esp_flash_*` functions |
|
||||
| GPIO | `driver/gpio.h` | `driver/gpio.h` | `gpio_pad_select_gpio()` removed |
|
||||
| Timer | `driver/timer.h` | `driver/gptimer.h` | General-purpose timer handles |
|
||||
| PCNT | `driver/pcnt.h` | `driver/pulse_cnt.h` | Handle-based API |
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
Generated
+8
-8
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "wled",
|
||||
"version": "16.0.0",
|
||||
"version": "17.0.0-dev",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "wled",
|
||||
"version": "16.0.0",
|
||||
"version": "17.0.0-dev",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"clean-css": "^5.3.3",
|
||||
@@ -132,9 +132,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz",
|
||||
"integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==",
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
|
||||
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^4.0.2"
|
||||
@@ -611,9 +611,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wled",
|
||||
"version": "16.0.0",
|
||||
"version": "17.0.0-dev",
|
||||
"description": "Tools for WLED project",
|
||||
"main": "tools/cdata.js",
|
||||
"directories": {
|
||||
|
||||
+12
-73
@@ -4,77 +4,16 @@
|
||||
Import("env")
|
||||
from pathlib import Path
|
||||
|
||||
# Linker script fragment injected into the rodata output section of whichever
|
||||
# platform we're building for. Placed just before the end-of-rodata marker so
|
||||
# that the dynarray entries land in flash rodata and are correctly sorted.
|
||||
DYNARRAY_INJECTION = (
|
||||
"\n /* dynarray: WLED dynamic module arrays */\n"
|
||||
" . = ALIGN(0x10);\n"
|
||||
" KEEP(*(SORT_BY_INIT_PRIORITY(.dynarray.*)))\n"
|
||||
" "
|
||||
)
|
||||
|
||||
|
||||
def inject_before_marker(path, marker):
|
||||
"""Patch a linker script file in-place, inserting DYNARRAY_INJECTION before marker."""
|
||||
original = path.read_text()
|
||||
path.write_text(original.replace(marker, DYNARRAY_INJECTION + marker, 1))
|
||||
|
||||
|
||||
if env.get("PIOPLATFORM") == "espressif32":
|
||||
# Find sections.ld on the linker search path (LIBPATH).
|
||||
sections_ld_path = None
|
||||
for ld_dir in env.get("LIBPATH", []):
|
||||
candidate = Path(str(ld_dir)) / "sections.ld"
|
||||
if candidate.exists():
|
||||
sections_ld_path = candidate
|
||||
break
|
||||
|
||||
if sections_ld_path is not None:
|
||||
# Inject inside the existing .flash.rodata output section, just before
|
||||
# _rodata_end. IDF v5 enforces zero gaps between adjacent output
|
||||
# sections via ASSERT statements, so INSERT AFTER .flash.rodata would
|
||||
# fail. Injecting inside the section creates no new output section and
|
||||
# leaves the ASSERTs satisfied.
|
||||
build_dir = Path(env.subst("$BUILD_DIR"))
|
||||
patched_path = build_dir / "dynarray_sections.ld"
|
||||
import shutil
|
||||
shutil.copy(sections_ld_path, patched_path)
|
||||
inject_before_marker(patched_path, "_rodata_end = ABSOLUTE(.);")
|
||||
|
||||
# Replace "sections.ld" in LINKFLAGS with an absolute path to our
|
||||
# patched copy. The flag may appear as a bare token, combined as
|
||||
# "-Tsections.ld", or split across two tokens ("-T", "sections.ld").
|
||||
patched_str = str(patched_path)
|
||||
new_flags = []
|
||||
skip_next = False
|
||||
for flag in env.get("LINKFLAGS", []):
|
||||
if skip_next:
|
||||
new_flags.append(patched_str if flag == "sections.ld" else flag)
|
||||
skip_next = False
|
||||
elif flag == "-T":
|
||||
new_flags.append(flag)
|
||||
skip_next = True
|
||||
else:
|
||||
new_flags.append(flag.replace("sections.ld", patched_str))
|
||||
env.Replace(LINKFLAGS=new_flags)
|
||||
platform = env.get("PIOPLATFORM")
|
||||
script_file = Path(f"tools/dynarray_{platform}.ld")
|
||||
if script_file.is_file():
|
||||
linker_script = f"-T{script_file}"
|
||||
if platform == "espressif32":
|
||||
# For ESP32, the script must be added at the right point in the list
|
||||
linkflags = env.get("LINKFLAGS", [])
|
||||
idx = linkflags.index("memory.ld")
|
||||
linkflags.insert(idx+1, linker_script)
|
||||
env.Replace(LINKFLAGS=linkflags)
|
||||
else:
|
||||
# Assume sections.ld will be built (ESP-IDF format); add a post-action to patch it
|
||||
# TODO: consider using ESP-IDF linker fragment (https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-guides/linker-script-generation.html)
|
||||
# For now, patch after building
|
||||
sections_ld = Path(env.subst("$BUILD_DIR")) / "sections.ld"
|
||||
def patch_sections_ld(target, source, env):
|
||||
inject_before_marker(sections_ld, "_rodata_end = ABSOLUTE(.);")
|
||||
env.AddPostAction(str(sections_ld), patch_sections_ld)
|
||||
|
||||
elif env.get("PIOPLATFORM") == "espressif8266":
|
||||
# The ESP8266 framework preprocesses eagle.app.v6.common.ld.h into
|
||||
# local.eagle.app.v6.common.ld in $BUILD_DIR/ld/ at build time. Register
|
||||
# a post-action on that generated file so the injection happens after
|
||||
# C-preprocessing but before linking.
|
||||
build_ld = Path(env.subst("$BUILD_DIR")) / "ld" / "local.eagle.app.v6.common.ld"
|
||||
|
||||
def patch_esp8266_ld(target, source, env):
|
||||
inject_before_marker(build_ld, "_irom0_text_end = ABSOLUTE(.);")
|
||||
|
||||
env.AddPostAction(str(build_ld), patch_esp8266_ld)
|
||||
# For other platforms, put it in last
|
||||
env.Append(LINKFLAGS=[linker_script])
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from pathlib import Path # For OS-agnostic path manipulation
|
||||
from click import secho
|
||||
from SCons.Script import Action, Exit
|
||||
Import("env")
|
||||
|
||||
_ATTR = re.compile(r'\bDW_AT_(name|comp_dir)\b')
|
||||
|
||||
|
||||
def read_lines(p: Path):
|
||||
""" Read in the contents of a file for analysis """
|
||||
@@ -14,129 +13,86 @@ def read_lines(p: Path):
|
||||
return f.readlines()
|
||||
|
||||
|
||||
def _get_readelf_path(env) -> str:
|
||||
""" Derive the readelf tool path from the build environment """
|
||||
# Derive from the C compiler: xtensa-esp32-elf-gcc → xtensa-esp32-elf-readelf
|
||||
cc = Path(env.subst("$CC"))
|
||||
return str(cc.with_name(re.sub(r'(gcc|g\+\+)$', 'readelf', cc.name)))
|
||||
def _get_nm_path(env) -> str:
|
||||
""" Derive the nm tool path from the build environment """
|
||||
if "NM" in env:
|
||||
return env.subst("$NM")
|
||||
# Derive from the C compiler: xtensa-esp32-elf-gcc → xtensa-esp32-elf-nm
|
||||
cc = env.subst("$CC")
|
||||
nm = re.sub(r'(gcc|g\+\+)$', 'nm', os.path.basename(cc))
|
||||
return os.path.join(os.path.dirname(cc), nm)
|
||||
|
||||
|
||||
def check_elf_modules(elf_path: Path, env, module_lib_builders) -> set[str]:
|
||||
""" Check which modules have at least one compilation unit in the ELF.
|
||||
""" Check which modules have at least one defined symbol placed in the ELF.
|
||||
|
||||
The map file is not a reliable source for this: with LTO, original object
|
||||
file paths are replaced by temporary ltrans.o partitions in all output
|
||||
sections, making per-module attribution impossible from the map alone.
|
||||
Instead we invoke readelf --debug-dump=info --dwarf-depth=1 on the ELF,
|
||||
which reads only the top-level compilation-unit DIEs from .debug_info.
|
||||
Each CU corresponds to one source file; matching DW_AT_comp_dir +
|
||||
DW_AT_name against the module src_dirs is sufficient to confirm a module
|
||||
was compiled into the ELF. The output volume is proportional to the
|
||||
number of source files, not the number of symbols.
|
||||
Instead we invoke nm --defined-only -l on the ELF, which uses DWARF debug
|
||||
info to attribute each placed symbol to its original source file.
|
||||
|
||||
Requires usermod libraries to be compiled with -g so that DWARF sections
|
||||
are present in the ELF. load_usermods.py injects -g for all WLED modules
|
||||
via dep.env.AppendUnique(CCFLAGS=["-g"]).
|
||||
|
||||
Returns the set of build_dir basenames for confirmed modules.
|
||||
"""
|
||||
readelf_path = _get_readelf_path(env)
|
||||
secho(f"INFO: Checking for usermod compilation units...")
|
||||
|
||||
nm_path = _get_nm_path(env)
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[readelf_path, "--debug-dump=info", "--dwarf-depth=1", str(elf_path)],
|
||||
[nm_path, "--defined-only", "-l", str(elf_path)],
|
||||
capture_output=True, text=True, errors="ignore", timeout=120,
|
||||
)
|
||||
output = result.stdout
|
||||
nm_output = result.stdout
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError, OSError) as e:
|
||||
secho(f"WARNING: readelf failed ({e}); skipping per-module validation", fg="yellow", err=True)
|
||||
secho(f"WARNING: nm failed ({e}); skipping per-module validation", fg="yellow", err=True)
|
||||
return {Path(b.build_dir).name for b in module_lib_builders} # conservative pass
|
||||
|
||||
# Match placed symbols against builders as we parse nm output, exiting early
|
||||
# once all builders are accounted for.
|
||||
# nm --defined-only still includes debugging symbols (type 'N') such as the
|
||||
# per-CU markers GCC emits in .debug_info (e.g. "usermod_example_cpp_6734d48d").
|
||||
# These live at address 0x00000000 in their debug section — not in any load
|
||||
# segment — so filtering them out leaves only genuinely placed symbols.
|
||||
# nm -l appends a tab-separated "file:lineno" location to each symbol line.
|
||||
remaining = {Path(str(b.src_dir)): Path(b.build_dir).name for b in module_lib_builders}
|
||||
found = set()
|
||||
project_dir = Path(env.subst("$PROJECT_DIR"))
|
||||
|
||||
def _flush_cu(comp_dir: str | None, name: str | None) -> None:
|
||||
"""Match one completed CU against remaining builders."""
|
||||
if not name or not remaining:
|
||||
return
|
||||
p = Path(name)
|
||||
src_path = (Path(comp_dir) / p) if (comp_dir and not p.is_absolute()) else p
|
||||
# In arduino+espidf dual-framework builds the IDF toolchain sets DW_AT_comp_dir
|
||||
# to the virtual path "/IDF_PROJECT" rather than the real project root, so
|
||||
# src_path won't match. Pre-compute a fallback using $PROJECT_DIR and check
|
||||
# both candidates in a single pass.
|
||||
use_fallback = not p.is_absolute() and comp_dir and Path(comp_dir) != project_dir
|
||||
src_path_real = project_dir / p if use_fallback else None
|
||||
for src_dir in list(remaining):
|
||||
if src_path.is_relative_to(src_dir) or (src_path_real and src_path_real.is_relative_to(src_dir)):
|
||||
found.add(remaining.pop(src_dir))
|
||||
return
|
||||
|
||||
# readelf emits one DW_TAG_compile_unit DIE per source file. Attributes
|
||||
# of interest:
|
||||
# DW_AT_name — source file (absolute, or relative to comp_dir)
|
||||
# DW_AT_comp_dir — compile working directory
|
||||
# Both appear as either a direct string or an indirect string:
|
||||
# DW_AT_name : foo.cpp
|
||||
# DW_AT_name : (indirect string, offset: 0x…): foo.cpp
|
||||
# Taking the portion after the *last* ": " on the line handles both forms.
|
||||
|
||||
comp_dir = name = None
|
||||
for line in output.splitlines():
|
||||
if 'Compilation Unit @' in line:
|
||||
_flush_cu(comp_dir, name)
|
||||
comp_dir = name = None
|
||||
continue
|
||||
for line in nm_output.splitlines():
|
||||
if not remaining:
|
||||
break # all builders matched
|
||||
m = _ATTR.search(line)
|
||||
if m:
|
||||
_, _, val = line.rpartition(': ')
|
||||
val = val.strip()
|
||||
if m.group(1) == 'name':
|
||||
name = val
|
||||
else:
|
||||
comp_dir = val
|
||||
_flush_cu(comp_dir, name) # flush the last CU
|
||||
addr, _, _ = line.partition(' ')
|
||||
if not addr.lstrip('0'):
|
||||
continue # zero address — skip debug-section marker
|
||||
if '\t' not in line:
|
||||
continue
|
||||
loc = line.rsplit('\t', 1)[1]
|
||||
# Strip trailing :lineno (e.g. "/path/to/foo.cpp:42" → "/path/to/foo.cpp")
|
||||
src_path = Path(loc.rsplit(':', 1)[0])
|
||||
# Path.is_relative_to() handles OS-specific separators correctly without
|
||||
# any regex, avoiding Windows path escaping issues.
|
||||
for src_dir in list(remaining):
|
||||
if src_path.is_relative_to(src_dir):
|
||||
found.add(remaining.pop(src_dir))
|
||||
break
|
||||
|
||||
return found
|
||||
|
||||
|
||||
DYNARRAY_SECTION = ".dtors" if env.get("PIOPLATFORM") == "espressif8266" else ".dynarray"
|
||||
USERMODS_SECTION = f"{DYNARRAY_SECTION}.usermods.1"
|
||||
|
||||
def count_usermod_objects(map_file: list[str]) -> int:
|
||||
""" Returns the number of usermod objects in the usermod list.
|
||||
|
||||
Computes the count from the address span between the .dynarray.usermods.0
|
||||
and .dynarray.usermods.99999 sentinel sections. This mirrors the
|
||||
DYNARRAY_LENGTH macro and is reliable under LTO, where all entries are
|
||||
merged into a single ltrans partition so counting section occurrences
|
||||
always yields 1 regardless of the true count.
|
||||
"""
|
||||
ENTRY_SIZE = 4 # sizeof(Usermod*) on 32-bit targets
|
||||
addr_begin = None
|
||||
addr_end = None
|
||||
|
||||
for i, line in enumerate(map_file):
|
||||
stripped = line.strip()
|
||||
if stripped == '.dynarray.usermods.0':
|
||||
if i + 1 < len(map_file):
|
||||
m = re.search(r'0x([0-9a-fA-F]+)', map_file[i + 1])
|
||||
if m:
|
||||
addr_begin = int(m.group(1), 16)
|
||||
elif stripped == '.dynarray.usermods.99999':
|
||||
if i + 1 < len(map_file):
|
||||
m = re.search(r'0x([0-9a-fA-F]+)', map_file[i + 1])
|
||||
if m:
|
||||
addr_end = int(m.group(1), 16)
|
||||
if addr_begin is not None and addr_end is not None:
|
||||
break
|
||||
|
||||
if addr_begin is None or addr_end is None:
|
||||
return 0
|
||||
return (addr_end - addr_begin) // ENTRY_SIZE
|
||||
""" Returns the number of usermod objects in the usermod list """
|
||||
# Count the number of entries in the usermods table section
|
||||
return len([x for x in map_file if USERMODS_SECTION in x])
|
||||
|
||||
|
||||
def validate_map_file(source, target, env):
|
||||
""" Validate that all modules appear in the output build """
|
||||
build_dir = Path(env.subst("$BUILD_DIR"))
|
||||
map_file_path = build_dir / env.subst("${PROGNAME}.map")
|
||||
map_file_path = build_dir / env.subst("${PROGNAME}.map")
|
||||
|
||||
if not map_file_path.exists():
|
||||
secho(f"ERROR: Map file not found: {map_file_path}", fg="red", err=True)
|
||||
@@ -155,7 +111,6 @@ def validate_map_file(source, target, env):
|
||||
secho(f"INFO: {usermod_object_count} usermod object entries")
|
||||
|
||||
elf_path = build_dir / env.subst("${PROGNAME}.elf")
|
||||
|
||||
confirmed_modules = check_elf_modules(elf_path, env, module_lib_builders)
|
||||
|
||||
missing_modules = [modname for mdir, modname in modules.items() if mdir not in confirmed_modules]
|
||||
@@ -165,6 +120,7 @@ def validate_map_file(source, target, env):
|
||||
fg="red",
|
||||
err=True)
|
||||
Exit(1)
|
||||
return None
|
||||
|
||||
env.Append(LINKFLAGS=[env.subst("-Wl,--Map=${BUILD_DIR}/${PROGNAME}.map")])
|
||||
env.AddPostAction("$BUILD_DIR/${PROGNAME}.elf", Action(validate_map_file, cmdstr='Checking linked optional modules (usermods) in map file'))
|
||||
|
||||
@@ -38,7 +38,6 @@ data_dir = ./wled00/data
|
||||
build_cache_dir = ~/.buildcache
|
||||
extra_configs =
|
||||
platformio_override.ini
|
||||
platformio_release.ini
|
||||
|
||||
[common]
|
||||
# ------------------------------------------------------------------------------
|
||||
@@ -222,7 +221,6 @@ lib_deps =
|
||||
ESPAsyncTCP @ 1.2.2
|
||||
ESPAsyncUDP
|
||||
ESP8266PWM
|
||||
https://github.com/tignioj/ArduinoUZlib.git#20aff95cd80c141f80bdbf66895409a0046d2c2f
|
||||
${env.lib_deps}
|
||||
|
||||
;; compatibilty flags - same as 0.14.0 which seems to work better on some 8266 boards. Not using PIO_FRAMEWORK_ARDUINO_MMU_CACHE16_IRAM48
|
||||
@@ -253,7 +251,6 @@ lib_deps_compat =
|
||||
IRremoteESP8266 @ 2.8.2
|
||||
makuna/NeoPixelBus @ 2.7.9
|
||||
https://github.com/blazoncek/QuickESPNow.git#optional-debug
|
||||
https://github.com/tignioj/ArduinoUZlib.git#20aff95cd80c141f80bdbf66895409a0046d2c2f
|
||||
https://github.com/Aircoookie/ESPAsyncWebServer.git#v2.4.0
|
||||
|
||||
[esp32_all_variants]
|
||||
@@ -378,7 +375,6 @@ build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=
|
||||
-D WLED_DISABLE_PARTICLESYSTEM2D
|
||||
lib_deps = ${esp8266.lib_deps}
|
||||
monitor_filters = esp8266_exception_decoder
|
||||
custom_usermods = audioreactive
|
||||
|
||||
[env:nodemcuv2_compat]
|
||||
extends = env:nodemcuv2
|
||||
@@ -388,7 +384,6 @@ platform_packages = ${esp8266.platform_packages_compat}
|
||||
build_flags = ${common.build_flags} ${esp8266.build_flags_compat} -D WLED_RELEASE_NAME=\"ESP8266_compat\" #-DWLED_DISABLE_2D
|
||||
-D WLED_DISABLE_PARTICLESYSTEM2D
|
||||
;; lib_deps = ${esp8266.lib_deps_compat} ;; experimental - use older NeoPixelBus 2.7.9
|
||||
custom_usermods = audioreactive
|
||||
|
||||
[env:nodemcuv2_160]
|
||||
extends = env:nodemcuv2
|
||||
@@ -407,7 +402,6 @@ build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=
|
||||
-D WLED_DISABLE_PARTICLESYSTEM2D
|
||||
-D WLED_DISABLE_PARTICLESYSTEM1D
|
||||
lib_deps = ${esp8266.lib_deps}
|
||||
custom_usermods = audioreactive
|
||||
|
||||
[env:esp8266_2m_compat]
|
||||
extends = env:esp8266_2m
|
||||
@@ -417,7 +411,6 @@ platform_packages = ${esp8266.platform_packages_compat}
|
||||
build_flags = ${common.build_flags} ${esp8266.build_flags_compat} -D WLED_RELEASE_NAME=\"ESP02_compat\" #-DWLED_DISABLE_2D
|
||||
-D WLED_DISABLE_PARTICLESYSTEM1D
|
||||
-D WLED_DISABLE_PARTICLESYSTEM2D
|
||||
custom_usermods = audioreactive
|
||||
|
||||
[env:esp8266_2m_160]
|
||||
extends = env:esp8266_2m
|
||||
@@ -720,89 +713,3 @@ monitor_filters = esp32_exception_decoder
|
||||
board_build.flash_mode = dio
|
||||
custom_usermods = * ; Expands to all usermods in usermods folder
|
||||
board_build.partitions = ${esp32.extreme_partitions} ; We're gonna need a bigger boat
|
||||
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Hub75 examples
|
||||
# ------------------------------------------------------------------------------
|
||||
# Note: some panels may experience ghosting with default full brightness. use -D WLED_HUB75_MAX_BRIGHTNESS=239 or lower to fix it.
|
||||
|
||||
[hub75]
|
||||
;; Shared values for all HUB75 build envs.
|
||||
|
||||
;; Core HUB75 flags - common to every HUB75 build
|
||||
build_flags =
|
||||
-D WLED_ENABLE_HUB75MATRIX -D NO_GFX
|
||||
-D WLED_DEBUG_BUS
|
||||
-D LED_TYPES=TYPE_HUB75MATRIX_HS
|
||||
; -D WLED_DEBUG
|
||||
|
||||
;; Default I2S mic pins disabled (HUB75 uses GPIOs that would otherwise clash).
|
||||
;; Envs that wire up a real mic should NOT include this and define I2S_*PIN themselves.
|
||||
i2s_disable_flags = -D SR_DMTYPE=1 -D I2S_SDPIN=-1 -D I2S_CKPIN=-1 -D I2S_WSPIN=-1 -D MCLK_PIN=-1 ;; Disable to prevent pin clash
|
||||
|
||||
;; Pinned HUB75 driver libraries
|
||||
lib_deps = https://github.com/mrfaptastic/ESP32-HUB75-MatrixPanel-DMA.git#f17fb7fe9d487e9643f919eb5aeedea8d9d1f8d7 ;; 3.0.14
|
||||
|
||||
;; Extra flags shared by all S3-based HUB75 builds
|
||||
s3_build_flags =
|
||||
-DARDUINO_USB_CDC_ON_BOOT=1 ;; -DARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB")
|
||||
-DBOARD_HAS_PSRAM
|
||||
-DLOLIN_WIFI_FIX ; seems to work much better with this (sets lower TX power)
|
||||
-D WLED_WATCHDOG_TIMEOUT=0
|
||||
-D S3_LCD_DIV_NUM=20 ;; Attempt to fix wifi performance issue when panel active with S3 chips
|
||||
|
||||
|
||||
[env:esp32dev_hub75]
|
||||
extends = env:esp32dev
|
||||
upload_speed = 921600
|
||||
build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} ${hub75.build_flags} ${hub75.i2s_disable_flags}
|
||||
-DARDUINO_USB_CDC_ON_BOOT=0 ;; this flag is mandatory for "classic ESP32" when building with arduino-esp32 >=2.0.3
|
||||
-D WLED_RELEASE_NAME=\"ESP32_HUB75\"
|
||||
lib_deps = ${esp32_idf_V4.lib_deps}
|
||||
${hub75.lib_deps}
|
||||
|
||||
[env:esp32dev_hub75_forum_pinout]
|
||||
extends = env:esp32dev_hub75
|
||||
build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} ${hub75.build_flags} ${hub75.i2s_disable_flags}
|
||||
-DARDUINO_USB_CDC_ON_BOOT=0
|
||||
-D WLED_RELEASE_NAME=\"ESP32_HUB75_forum_pinout\"
|
||||
-D ESP32_FORUM_PINOUT ;; enable for SmartMatrix default pins
|
||||
|
||||
[env:esp32s3dev_4MB_qspi_hub75]
|
||||
; HD-WF2 - NOTE: this board has NO PSRAM, so BOARD_HAS_PSRAM must not be set
|
||||
; (BOARD_HAS_PSRAM causes the DMA library to allocate only in SPIRAM, which fails without PSRAM)
|
||||
extends = env:esp32s3dev_8MB_qspi
|
||||
board_build.partitions = ${esp32.extended_partitions} ;; 1.65MB firmware, 700KB filesystem
|
||||
build_unflags = -DBOARD_HAS_PSRAM
|
||||
build_flags = ${common.build_flags} ${esp32s3.build_flags} ${hub75.build_flags} ${hub75.s3_build_flags} ${hub75.i2s_disable_flags}
|
||||
-D WLED_RELEASE_NAME=\"ESP32-S3_HD-WF2\"
|
||||
-D HD_WF2_PINOUT ;; Huidu HD-WF2 specific GPIO wiring
|
||||
lib_deps = ${esp32s3.lib_deps}
|
||||
${hub75.lib_deps}
|
||||
|
||||
[env:adafruit_matrixportal_esp32s3]
|
||||
; ESP32-S3 processor, 8 MB flash, 2 MB of PSRAM, dedicated driver pins for HUB75
|
||||
extends = env:esp32s3dev_8MB_qspi
|
||||
board = adafruit_matrixportal_esp32s3_wled ; modified board definition: removed flash section that causes FS erase on upload
|
||||
build_flags = ${common.build_flags} ${esp32s3.build_flags} ${hub75.build_flags} ${hub75.s3_build_flags} ${hub75.i2s_disable_flags}
|
||||
-D WLED_RELEASE_NAME=\"ESP32-S3_Adafruit_Matrixportal\"
|
||||
-D ARDUINO_ADAFRUIT_MATRIXPORTAL_ESP32S3
|
||||
lib_deps = ${esp32s3.lib_deps}
|
||||
${hub75.lib_deps}
|
||||
;; board_build.partitions = tools/partitions-8MB_spiffs-tinyuf2.csv ;; supports adafruit UF2 bootloader
|
||||
|
||||
[env:esp32s3dev_16MB_opi_hub75]
|
||||
;; MOONHUB HUB75 adapter board (lilygo T7-S3 with 16MB flash and octal PSRAM)
|
||||
extends = env:esp32s3dev_8MB_opi
|
||||
board = lilygo-t7-s3
|
||||
board_build.partitions = ${esp32.extreme_partitions} ;; for 16MB flash (overrides large_partitions for 8MB)
|
||||
;; Note: real I2S mic pins are wired here, so we do NOT include ${hub75.i2s_disable_flags}.
|
||||
build_flags = ${common.build_flags} ${esp32s3.build_flags} ${hub75.build_flags} ${hub75.s3_build_flags}
|
||||
-D WLED_RELEASE_NAME=\"ESP32-S3_16MB_opi_HUB75\"
|
||||
-D MOONHUB_S3_PINOUT ;; HUB75 pinout
|
||||
-D LEDPIN=14 -D BTNPIN=0 -D RLYPIN=15 -D IRPIN=-1 -D AUDIOPIN=-1 ;; defaults that avoid pin conflicts with HUB75
|
||||
-D SR_DMTYPE=1 -D I2S_SDPIN=10 -D I2S_CKPIN=11 -D I2S_WSPIN=12 -D MCLK_PIN=-1 ;; I2S mic
|
||||
lib_deps = ${esp32s3.lib_deps}
|
||||
${hub75.lib_deps}
|
||||
|
||||
@@ -193,8 +193,7 @@ build_flags = ${common.build_flags} ${esp8266.build_flags}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Optional: build flags for speed, instead of optimising for size.
|
||||
# Add ${Speed_Flags.build_flags} / ${Speed_Flags.build_unflags} to your own env
|
||||
# in platformio_override.ini to opt in.
|
||||
# Example of usage: see [env:esp32S3_PSRAM_HUB75]
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
[Speed_Flags]
|
||||
@@ -555,4 +554,102 @@ custom_usermods =
|
||||
https://github.com/wled/wled-usermod-example.git#main
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Hub75 examples
|
||||
# ------------------------------------------------------------------------------
|
||||
# Note: some panels may experience ghosting with default full brightness. use -D WLED_HUB75_MAX_BRIGHTNESS=239 or lower to fix it.
|
||||
|
||||
[env:esp32dev_hub75]
|
||||
board = esp32dev
|
||||
upload_speed = 921600
|
||||
platform = ${esp32_idf_V4.platform}
|
||||
platform_packages =
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = ${common.build_flags}
|
||||
-D WLED_RELEASE_NAME=\"ESP32_hub75\"
|
||||
-D WLED_ENABLE_HUB75MATRIX -D NO_GFX
|
||||
-D WLED_DEBUG_BUS
|
||||
; -D WLED_DEBUG
|
||||
-D SR_DMTYPE=1 -D I2S_SDPIN=-1 -D I2S_CKPIN=-1 -D I2S_WSPIN=-1 -D MCLK_PIN=-1 ;; Disable to prevent pin clash
|
||||
|
||||
lib_deps = ${esp32_idf_V4.lib_deps}
|
||||
https://github.com/mrfaptastic/ESP32-HUB75-MatrixPanel-DMA.git#3.0.11
|
||||
|
||||
monitor_filters = esp32_exception_decoder
|
||||
board_build.partitions = ${esp32.default_partitions}
|
||||
board_build.flash_mode = dio
|
||||
custom_usermods = audioreactive
|
||||
|
||||
[env:esp32dev_hub75_forum_pinout]
|
||||
extends = env:esp32dev_hub75
|
||||
build_flags = ${common.build_flags}
|
||||
-D WLED_RELEASE_NAME=\"ESP32_hub75_forum_pinout\"
|
||||
-D WLED_ENABLE_HUB75MATRIX -D NO_GFX
|
||||
-D ESP32_FORUM_PINOUT ;; enable for SmartMatrix default pins
|
||||
-D WLED_DEBUG_BUS
|
||||
-D SR_DMTYPE=1 -D I2S_SDPIN=-1 -D I2S_CKPIN=-1 -D I2S_WSPIN=-1 -D MCLK_PIN=-1 ;; Disable to prevent pin clash
|
||||
; -D WLED_DEBUG
|
||||
|
||||
|
||||
[env:adafruit_matrixportal_esp32s3]
|
||||
; ESP32-S3 processor, 8 MB flash, 2 MB of PSRAM, dedicated driver pins for HUB75
|
||||
board = adafruit_matrixportal_esp32s3_wled ; modified board definition: removed flash section that causes FS erase on upload
|
||||
;; adafruit recommends to use arduino-esp32 2.0.14
|
||||
;;platform = espressif32@ ~6.5.0
|
||||
;;platform_packages = platformio/framework-arduinoespressif32 @ 3.20014.231204 ;; arduino-esp32 2.0.14
|
||||
platform = ${esp32s3.platform}
|
||||
platform_packages =
|
||||
upload_speed = 921600
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_8M_qspi\"
|
||||
-DARDUINO_USB_CDC_ON_BOOT=1 ;; -DARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB")
|
||||
-DBOARD_HAS_PSRAM
|
||||
-DLOLIN_WIFI_FIX ; seems to work much better with this (sets lower TX power)
|
||||
-D WLED_WATCHDOG_TIMEOUT=0
|
||||
-D WLED_ENABLE_HUB75MATRIX -D NO_GFX
|
||||
-D S3_LCD_DIV_NUM=20 ;; Attempt to fix wifi performance issue when panel active with S3 chips
|
||||
-D ARDUINO_ADAFRUIT_MATRIXPORTAL_ESP32S3
|
||||
-D WLED_DEBUG_BUS
|
||||
-D SR_DMTYPE=1 -D I2S_SDPIN=-1 -D I2S_CKPIN=-1 -D I2S_WSPIN=-1 -D MCLK_PIN=-1 ;; Disable to prevent pin clash
|
||||
|
||||
|
||||
lib_deps = ${esp32s3.lib_deps}
|
||||
https://github.com/mrfaptastic/ESP32-HUB75-MatrixPanel-DMA.git#aa28e2a ;; S3_LCD_DIV_NUM fix
|
||||
|
||||
board_build.partitions = ${esp32.large_partitions} ;; standard bootloader and 8MB Flash partitions
|
||||
;; board_build.partitions = tools/partitions-8MB_spiffs-tinyuf2.csv ;; supports adafruit UF2 bootloader
|
||||
board_build.f_flash = 80000000L
|
||||
board_build.flash_mode = qio
|
||||
monitor_filters = esp32_exception_decoder
|
||||
custom_usermods = audioreactive
|
||||
|
||||
[env:esp32S3_PSRAM_HUB75]
|
||||
;; MOONHUB HUB75 adapter board (lilygo T7-S3 with 16MB flash and PSRAM)
|
||||
board = lilygo-t7-s3
|
||||
platform = ${esp32s3.platform}
|
||||
platform_packages =
|
||||
upload_speed = 921600
|
||||
build_unflags = ${common.build_unflags}
|
||||
${Speed_Flags.build_unflags} ;; optional: removes "-Os" so we can override with "-O2" in build_flags
|
||||
build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"esp32S3_16MB_PSRAM_HUB75\"
|
||||
${Speed_Flags.build_flags} ;; optional: -O2 -> optimize for speed instead of size
|
||||
-DARDUINO_USB_CDC_ON_BOOT=1 ;; -DARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB")
|
||||
-DBOARD_HAS_PSRAM
|
||||
-DLOLIN_WIFI_FIX ; seems to work much better with this (sets lower TX power)
|
||||
-D WLED_WATCHDOG_TIMEOUT=0
|
||||
-D WLED_ENABLE_HUB75MATRIX -D NO_GFX
|
||||
-D S3_LCD_DIV_NUM=20 ;; Attempt to fix wifi performance issue when panel active with S3 chips
|
||||
-D MOONHUB_S3_PINOUT ;; HUB75 pinout
|
||||
-D WLED_DEBUG_BUS
|
||||
-D LEDPIN=14 -D BTNPIN=0 -D RLYPIN=15 -D IRPIN=-1 -D AUDIOPIN=-1 ;; defaults that avoid pin conflicts with HUB75
|
||||
-D SR_DMTYPE=1 -D I2S_SDPIN=10 -D I2S_CKPIN=11 -D I2S_WSPIN=12 -D MCLK_PIN=-1 ;; I2S mic
|
||||
|
||||
lib_deps = ${esp32s3.lib_deps}
|
||||
https://github.com/mrfaptastic/ESP32-HUB75-MatrixPanel-DMA.git#aa28e2a ;; S3_LCD_DIV_NUM fix
|
||||
|
||||
;;board_build.partitions = ${esp32.large_partitions} ;; for 8MB flash
|
||||
board_build.partitions = ${esp32.extreme_partitions} ;; for 16MB flash
|
||||
board_build.f_flash = 80000000L
|
||||
board_build.flash_mode = qio
|
||||
monitor_filters = esp32_exception_decoder
|
||||
custom_usermods = audioreactive
|
||||
+1
-1
@@ -38,7 +38,7 @@ pyelftools==0.32
|
||||
# via platformio
|
||||
pyserial==3.5
|
||||
# via platformio
|
||||
requests==2.32.4
|
||||
requests==2.33.0
|
||||
# via platformio
|
||||
semantic-version==2.10.0
|
||||
# via platformio
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
/* ESP32 linker script fragment to add dynamic array section to binary */
|
||||
SECTIONS
|
||||
{
|
||||
.dynarray :
|
||||
{
|
||||
. = ALIGN(0x10);
|
||||
KEEP(*(SORT_BY_INIT_PRIORITY(.dynarray.*)))
|
||||
} > default_rodata_seg
|
||||
}
|
||||
INSERT AFTER .flash.rodata;
|
||||
@@ -140,7 +140,7 @@ static uint8_t binNum = 8; // Used to select the bin for FFT based bea
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3)
|
||||
#define UM_AUDIOREACTIVE_USE_INTEGER_FFT // always use integer FFT on ESP32-S2 and ESP32-C3
|
||||
#endif
|
||||
#endif // UM_AUDIOREACTIVE_USE_ARDUINO_FFT
|
||||
#endif
|
||||
|
||||
#if !defined(UM_AUDIOREACTIVE_USE_INTEGER_FFT)
|
||||
using FFTsampleType = float;
|
||||
@@ -296,17 +296,17 @@ void FFTcode(void * parameter)
|
||||
ArduinoFFT<float> FFT = ArduinoFFT<float>(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) {
|
||||
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
|
||||
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
|
||||
}
|
||||
// create window
|
||||
if (windowFFT == nullptr) {
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
if (dsps_fft2r_init_fc32(NULL, samplesFFT) != ESP_OK) return; // initialize FFT tables
|
||||
// create window function for FFT
|
||||
@@ -316,20 +316,16 @@ void FFTcode(void * parameter)
|
||||
dsps_wind_flat_top_f32(windowFFT, samplesFFT);
|
||||
#endif
|
||||
#else
|
||||
// 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);
|
||||
// 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
|
||||
// create window
|
||||
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*) 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 (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
|
||||
|
||||
// create window function for FFT
|
||||
float *windowFloat = (float*) calloc(sizeof(float), samplesFFT); // temporary buffer for window function
|
||||
if ((windowFloat == nullptr)) return; // something went wrong
|
||||
#ifdef FFT_PREFER_EXACT_PEAKS
|
||||
dsps_wind_blackman_harris_f32(windowFloat, samplesFFT);
|
||||
#else
|
||||
@@ -339,7 +335,7 @@ void FFTcode(void * parameter)
|
||||
for (int i = 0; i < samplesFFT; i++) {
|
||||
windowFFT[i] = (int16_t)(windowFloat[i] * 32767.0f);
|
||||
}
|
||||
heap_caps_free(windowFloat); // free temporary buffer
|
||||
free(windowFloat); // free temporary buffer
|
||||
#endif
|
||||
|
||||
// see https://www.freertos.org/vtaskdelayuntil.html
|
||||
@@ -472,6 +468,7 @@ 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
|
||||
@@ -761,8 +758,6 @@ class AudioReactive : public Usermod {
|
||||
private:
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
|
||||
static constexpr uint8_t SR_DMTYPE_NETWORK_ONLY = 254;
|
||||
|
||||
#ifndef AUDIOPIN
|
||||
int8_t audioPin = -1;
|
||||
#else
|
||||
@@ -884,9 +879,6 @@ class AudioReactive : public Usermod {
|
||||
#endif
|
||||
static const char _digitalmic[];
|
||||
static const char _addPalettes[];
|
||||
static const char _palName0[];
|
||||
static const char _palName1[];
|
||||
static const char _palName2[];
|
||||
static const char UDP_SYNC_HEADER[];
|
||||
static const char UDP_SYNC_HEADER_v1[];
|
||||
|
||||
@@ -1447,7 +1439,7 @@ class AudioReactive : public Usermod {
|
||||
break;
|
||||
#endif
|
||||
|
||||
case SR_DMTYPE_NETWORK_ONLY: // dummy "network receive only" mode
|
||||
case 254: // dummy "network receive only" mode
|
||||
if (audioSource) delete audioSource; audioSource = nullptr;
|
||||
disableSoundProcessing = true;
|
||||
audioSyncEnabled = 2; // force udp sound receive mode
|
||||
@@ -1464,25 +1456,19 @@ class AudioReactive : public Usermod {
|
||||
}
|
||||
delay(250); // give microphone enough time to initialise
|
||||
|
||||
if (!audioSource && (dmType != SR_DMTYPE_NETWORK_ONLY)) enabled = false;// audio failed to initialise
|
||||
if (!audioSource && (dmType != 254)) enabled = false;// audio failed to initialise
|
||||
#endif
|
||||
if (enabled) onUpdateBegin(false); // create FFT task, and initialize network
|
||||
|
||||
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
if (audioSource && FFT_Task == nullptr) enabled = false; // FFT task creation failed
|
||||
if (FFT_Task == nullptr) enabled = false; // FFT task creation failed
|
||||
if((!audioSource) || (!audioSource->isInitialized())) { // audio source failed to initialize. Still stay "enabled", as there might be input arriving via UDP Sound Sync
|
||||
#ifdef WLED_DEBUG
|
||||
#define AR_INIT_DEBUG_PRINT DEBUG_PRINTLN
|
||||
#else
|
||||
#define AR_INIT_DEBUG_PRINT DEBUGSR_PRINTLN
|
||||
#endif
|
||||
if (dmType == SR_DMTYPE_NETWORK_ONLY) {
|
||||
AR_INIT_DEBUG_PRINT(F("AR: No sound input driver configured - network receive only."));
|
||||
} else {
|
||||
AR_INIT_DEBUG_PRINT(F("AR: Failed to initialize sound input driver. Please check input PIN settings."));
|
||||
}
|
||||
#undef AR_INIT_DEBUG_PRINT
|
||||
#ifdef WLED_DEBUG
|
||||
DEBUG_PRINTLN(F("AR: Failed to initialize sound input driver. Please check input PIN settings."));
|
||||
#else
|
||||
DEBUGSR_PRINTLN(F("AR: Failed to initialize sound input driver. Please check input PIN settings."));
|
||||
#endif
|
||||
disableSoundProcessing = true;
|
||||
}
|
||||
#endif
|
||||
@@ -1965,10 +1951,14 @@ class AudioReactive : public Usermod {
|
||||
}
|
||||
#endif
|
||||
}
|
||||
if (palettes > 0 && root.containsKey(F("rmcpal"))) {
|
||||
// handle removal of custom palettes from JSON call so we don't break things
|
||||
removeAudioPalettes();
|
||||
}
|
||||
}
|
||||
|
||||
void onStateChange(uint8_t callMode) override {
|
||||
if (initDone && enabled && addPalettes && palettes==0) {
|
||||
if (initDone && enabled && addPalettes && palettes==0 && customPalettes.size()<WLED_MAX_CUSTOM_PALETTES) {
|
||||
// if palettes were removed during JSON call re-add them
|
||||
createAudioPalettes();
|
||||
}
|
||||
@@ -2133,9 +2123,6 @@ class AudioReactive : public Usermod {
|
||||
uiScript.print(F("addOption(dd,'Generic PDM',5);"));
|
||||
#endif
|
||||
uiScript.print(F("addOption(dd,'ES8388',6);"));
|
||||
uiScript.print(F("addOption(dd,'None - network receive only',"));
|
||||
uiScript.print(SR_DMTYPE_NETWORK_ONLY);
|
||||
uiScript.print(F(");"));
|
||||
|
||||
uiScript.print(F("dd=addDropdown(ux,'config:AGC');"));
|
||||
uiScript.print(F("addOption(dd,'Off',0);"));
|
||||
@@ -2200,21 +2187,24 @@ class AudioReactive : public Usermod {
|
||||
|
||||
void AudioReactive::removeAudioPalettes(void) {
|
||||
DEBUG_PRINTLN(F("Removing audio palettes."));
|
||||
palettes -= (int8_t)removeUsermodPalettes(_name);
|
||||
if (palettes < 0) palettes = 0; // safeguard
|
||||
while (palettes>0) {
|
||||
customPalettes.pop_back();
|
||||
DEBUG_PRINTLN(palettes);
|
||||
palettes--;
|
||||
}
|
||||
DEBUG_PRINT(F("Total # of palettes: ")); DEBUG_PRINTLN(customPalettes.size());
|
||||
}
|
||||
|
||||
void AudioReactive::createAudioPalettes(void) {
|
||||
DEBUG_PRINT(F("Total # of palettes: ")); DEBUG_PRINTLN(customPalettes.size());
|
||||
if (palettes) return;
|
||||
DEBUG_PRINTLN(F("Adding audio palettes."));
|
||||
static const char *const palNames[MAX_PALETTES] PROGMEM = {_palName0, _palName1, _palName2};
|
||||
for (int i=0; i<MAX_PALETTES; i++) {
|
||||
if (usermodPalettes.size() < WLED_MAX_USERMOD_PALETTES) {
|
||||
usermodPalettes.push_back({CRGBPalette16(CRGB(BLACK)), _name, (uint8_t)i, palNames[i]}); // start black, filled each loop by fillAudioPalettes()
|
||||
for (int i=0; i<MAX_PALETTES; i++)
|
||||
if (customPalettes.size() < WLED_MAX_CUSTOM_PALETTES) {
|
||||
customPalettes.push_back(CRGBPalette16(CRGB(BLACK)));
|
||||
palettes++;
|
||||
DEBUG_PRINTLN(palettes);
|
||||
} else break;
|
||||
}
|
||||
}
|
||||
|
||||
// credit @netmindz ar palette, adapted for usermod @blazoncek
|
||||
@@ -2248,10 +2238,9 @@ CRGB AudioReactive::getCRGBForBand(int x, int pal) {
|
||||
|
||||
void AudioReactive::fillAudioPalettes() {
|
||||
if (!palettes) return;
|
||||
// Scan by name pointer identity to find the palettes we added, palIndex = 0/1/2... selects the getCRGBForBand variant, matching how the entries were created.
|
||||
for (auto &ump : usermodPalettes) {
|
||||
if (ump.name != _name) continue;
|
||||
const int pal = ump.palIndex;
|
||||
size_t lastCustPalette = customPalettes.size();
|
||||
if (int(lastCustPalette) >= palettes) lastCustPalette -= palettes;
|
||||
for (int pal=0; pal<palettes; pal++) {
|
||||
uint8_t tcp[16]; // Needs to be 4 times however many colors are being used.
|
||||
// 3 colors = 12, 4 colors = 16, etc.
|
||||
|
||||
@@ -2259,26 +2248,26 @@ void AudioReactive::fillAudioPalettes() {
|
||||
tcp[1] = 0;
|
||||
tcp[2] = 0;
|
||||
tcp[3] = 0;
|
||||
|
||||
|
||||
CRGB rgb = getCRGBForBand(1, pal);
|
||||
tcp[4] = 1; // anchor of first color
|
||||
tcp[5] = rgb.r;
|
||||
tcp[6] = rgb.g;
|
||||
tcp[7] = rgb.b;
|
||||
|
||||
|
||||
rgb = getCRGBForBand(128, pal);
|
||||
tcp[8] = 128;
|
||||
tcp[9] = rgb.r;
|
||||
tcp[10] = rgb.g;
|
||||
tcp[11] = rgb.b;
|
||||
|
||||
|
||||
rgb = getCRGBForBand(255, pal);
|
||||
tcp[12] = 255; // anchor of last color - must be 255
|
||||
tcp[13] = rgb.r;
|
||||
tcp[14] = rgb.g;
|
||||
tcp[15] = rgb.b;
|
||||
|
||||
ump.palette.loadDynamicGradientPalette(tcp);
|
||||
customPalettes[lastCustPalette+pal].loadDynamicGradientPalette(tcp);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2293,10 +2282,7 @@ const char AudioReactive::_inputLvl[] PROGMEM = "inputLevel";
|
||||
const char AudioReactive::_analogmic[] PROGMEM = "analogmic";
|
||||
#endif
|
||||
const char AudioReactive::_digitalmic[] PROGMEM = "digitalmic";
|
||||
const char AudioReactive::_addPalettes[] PROGMEM = "add-palettes";
|
||||
const char AudioReactive::_palName0[] PROGMEM = "Ratio";
|
||||
const char AudioReactive::_palName1[] PROGMEM = "Hue";
|
||||
const char AudioReactive::_palName2[] PROGMEM = "Spectrum";
|
||||
const char AudioReactive::_addPalettes[] PROGMEM = "add-palettes";
|
||||
const char AudioReactive::UDP_SYNC_HEADER[] PROGMEM = "00002"; // new sync header version, as format no longer compatible with previous structure
|
||||
const char AudioReactive::UDP_SYNC_HEADER_v1[] PROGMEM = "00001"; // old sync header version - need to add backwards-compatibility feature
|
||||
|
||||
|
||||
+4
-8
@@ -701,9 +701,7 @@ void dissolve(uint32_t color) {
|
||||
unsigned i = hw_random16(SEGLEN);
|
||||
if (SEGENV.aux0) { //dissolve to primary/palette
|
||||
if (pixels[i] == SEGCOLOR(1)) {
|
||||
uint32_t c = color == SEGCOLOR(0) ? SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0) : color;
|
||||
if (SEGMENT.check2 && c == SEGCOLOR(1)) c ^= 0x00000001; // change the color slightly so the effect doesn't get stuck in Complete mode if color is same as bkg color
|
||||
pixels[i] = c;
|
||||
pixels[i] = color == SEGCOLOR(0) ? SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0) : color;
|
||||
break; //only spawn 1 new pixel per frame
|
||||
}
|
||||
} else { //dissolve to secondary
|
||||
@@ -5453,7 +5451,6 @@ void mode_2Dgameoflife(void) { // Written by Ewoud Wijma, inspired by https://na
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Repeat detection
|
||||
@@ -8733,7 +8730,7 @@ void mode_particleperlin(void) {
|
||||
|
||||
PartSys->update(); // update and render
|
||||
}
|
||||
static const char _data_FX_MODE_PARTICLEPERLIN[] PROGMEM = "PS Fuzzy Noise@Speed,Particles,Bounce,Friction,Scale,Cylinder,Smear,Collide;;!;2;pal=64,sx=50,ix=200,c1=130,c2=30,c3=5";
|
||||
static const char _data_FX_MODE_PARTICLEPERLIN[] PROGMEM = "PS Fuzzy Noise@Speed,Particles,Bounce,Friction,Scale,Cylinder,Smear,Collide;;!;2;pal=64,sx=50,ix=200,c1=130,c2=30,c3=5,o3=1";
|
||||
|
||||
/*
|
||||
Particle smashing down like meteors and exploding as they hit the ground, has many parameters to play with
|
||||
@@ -9839,7 +9836,6 @@ void mode_particleFireworks1D(void) {
|
||||
PartSys->setColorByPosition(false); // disable
|
||||
for (uint32_t e = 0; e < explosionsize; e++) { // emit explosion particles
|
||||
int idx = PartSys->sprayEmit(PartSys->sources[0]); // emit a particle
|
||||
if (idx < 0) break; // no more particles available
|
||||
if(SEGMENT.custom3 > 23) {
|
||||
if(SEGMENT.custom3 == 31) { // highest slider value
|
||||
PartSys->setColorByAge(SEGMENT.check1); // color by age if colorful mode is enabled
|
||||
@@ -9864,7 +9860,7 @@ void mode_particleFireworks1D(void) {
|
||||
PartSys->applyFriction(1); // apply friction to all particles
|
||||
|
||||
PartSys->update(); // update and render
|
||||
|
||||
|
||||
for (uint32_t i = 0; i < PartSys->usedParticles; i++) {
|
||||
if (PartSys->particles[i].ttl > 20) PartSys->particles[i].ttl -= 20; //ttl is linked to brightness, this allows to use higher brightness but still a short spark lifespan
|
||||
else PartSys->particles[i].ttl = 0;
|
||||
@@ -9914,7 +9910,7 @@ void mode_particleSparkler(void) {
|
||||
PartSys->sources[i].source.ttl = 400; // replenish its life (setting it perpetual uses more code)
|
||||
PartSys->sources[i].sat = SEGMENT.custom1; // color saturation
|
||||
if (SEGMENT.speed == 255) // random position at highest speed setting
|
||||
PartSys->sources[i].source.x = hw_random(PartSys->maxX);
|
||||
PartSys->sources[i].source.x = hw_random16(PartSys->maxX);
|
||||
else
|
||||
PartSys->particleMoveUpdate(PartSys->sources[i].source, PartSys->sources[i].sourceFlags, &sparklersettings); //move sparkler
|
||||
}
|
||||
|
||||
+7
-22
@@ -228,19 +228,12 @@ void Segment::resetIfRequired() {
|
||||
void Segment::loadPalette(CRGBPalette16 &targetPalette, uint8_t pal) {
|
||||
// there is one randomly generated palette (1) followed by 4 palettes created from segment colors (2-5)
|
||||
// those are followed by 7 fastled palettes (6-12) and 59 gradient palettes (13-71)
|
||||
// then come user custom palettes (IDs <=200) and usermod palettes (IDs 201-255), both growing downward from their respective base IDs
|
||||
// then come the custom palettes (255,254,...) growing downwards from 255 (255 being 1st custom palette)
|
||||
// palette 0 is a varying palette depending on effect and may be replaced by segment's color if so
|
||||
// instructed in color_from_palette()
|
||||
if (pal == 0) pal = _default_palette; // _default_palette is set in setMode(), differs depending on effect
|
||||
const int umCount = usermodPalettes.size();
|
||||
const int custCount = customPalettes.size();
|
||||
if (pal >= FIXED_PALETTE_COUNT) {
|
||||
if (pal > WLED_CUSTOM_PALETTE_ID_BASE) { // usermod range (IDs 201-255)
|
||||
if ((WLED_USERMOD_PALETTE_ID_BASE - pal) >= umCount) pal = 0;
|
||||
} else { // custom range
|
||||
if ((WLED_CUSTOM_PALETTE_ID_BASE - pal) >= custCount) pal = 0;
|
||||
}
|
||||
}
|
||||
if (pal >= FIXED_PALETTE_COUNT && pal <= 255-customPalettes.size()) pal = 0; // out of bounds palette
|
||||
//default palette. Differs depending on effect
|
||||
if (pal == 0) pal = _default_palette; // _default_palette is set in setMode()
|
||||
switch (pal) {
|
||||
case 0: //default palette. Exceptions for specific effects above
|
||||
targetPalette = PartyColors_gc22;
|
||||
@@ -274,10 +267,8 @@ void Segment::loadPalette(CRGBPalette16 &targetPalette, uint8_t pal) {
|
||||
}
|
||||
break;}
|
||||
default: //progmem palettes
|
||||
if (pal > WLED_CUSTOM_PALETTE_ID_BASE) { // usermod palette
|
||||
targetPalette = usermodPalettes[WLED_USERMOD_PALETTE_ID_BASE - pal].palette;
|
||||
} else if (pal >= FIXED_PALETTE_COUNT) { // user custom palette
|
||||
targetPalette = customPalettes[WLED_CUSTOM_PALETTE_ID_BASE - pal];
|
||||
if (pal > 255 - customPalettes.size()) {
|
||||
targetPalette = customPalettes[255-pal]; // we checked bounds above
|
||||
} else if (pal < DYNAMIC_PALETTE_COUNT + FASTLED_PALETTE_COUNT) { // palette 6 - 12, fastled palettes
|
||||
targetPalette = *fastledPalettes[pal - DYNAMIC_PALETTE_COUNT];
|
||||
} else {
|
||||
@@ -594,13 +585,7 @@ Segment &Segment::setMode(uint8_t fx, bool loadDefaults) {
|
||||
}
|
||||
|
||||
Segment &Segment::setPalette(uint8_t pal) {
|
||||
if (pal >= FIXED_PALETTE_COUNT) {
|
||||
if (pal > WLED_CUSTOM_PALETTE_ID_BASE) { // usermod range
|
||||
if ((WLED_USERMOD_PALETTE_ID_BASE - pal) >= (int)usermodPalettes.size()) pal = 0;
|
||||
} else { // custom range
|
||||
if ((WLED_CUSTOM_PALETTE_ID_BASE - pal) >= (int)customPalettes.size()) pal = 0;
|
||||
}
|
||||
}
|
||||
if (pal <= 255-customPalettes.size() && pal > FIXED_PALETTE_COUNT) pal = 0; // not built in palette or custom palette
|
||||
if (pal != palette) {
|
||||
//DEBUG_PRINTF_P(PSTR("- Starting palette transition: %d\n"), pal);
|
||||
startTransition(strip.getTransition(), blendingStyle != TRANSITION_FADE); // start transition prior to change (no need to copy segment)
|
||||
|
||||
@@ -858,14 +858,6 @@ BusHub75Matrix::BusHub75Matrix(const BusConfig &bc) : Bus(bc.type, bc.start, bc.
|
||||
DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA - Matrix Portal S3 config");
|
||||
mxconfig.gpio = { 42, 41, 40, 38, 39, 37, 45, 36, 48, 35, 21, 47, 14, 2 };
|
||||
|
||||
#elif defined(HD_WF2_PINOUT) // Huidu HD-WF2 ESP32-S3 (no PSRAM)
|
||||
|
||||
// https://www.aliexpress.com/item/1005002258734810.html
|
||||
// https://github.com/mrcodetastic/ESP32-HUB75-MatrixPanel-DMA/issues/433
|
||||
DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA - HD-WF2 S3 config");
|
||||
// HUB75_I2S_CFG::i2s_pins _pins={R1_PIN, G1_PIN, B1_PIN, R2_PIN, G2_PIN, B2_PIN, A_PIN, B_PIN, C_PIN, D_PIN, E_PIN, LAT_PIN, OE_PIN, CLK_PIN};
|
||||
mxconfig.gpio = { 2, 6, 10, 3, 7, 11, 39, 38, 37, 36, 21, 33, 35, 34 };
|
||||
|
||||
#elif defined(CONFIG_IDF_TARGET_ESP32S3) && defined(BOARD_HAS_PSRAM)// ESP32-S3 with PSRAM
|
||||
|
||||
#if defined(MOONHUB_S3_PINOUT)
|
||||
@@ -1086,7 +1078,6 @@ uint32_t BusHub75Matrix::getPixelColor(unsigned pix) const {
|
||||
|
||||
void BusHub75Matrix::setBrightness(uint8_t b) {
|
||||
_bri = b;
|
||||
if (!_valid || !display) return;
|
||||
display->setBrightness(_bri);
|
||||
}
|
||||
|
||||
|
||||
@@ -171,7 +171,7 @@ class Bus {
|
||||
inline bool containsPixel(uint16_t pix) const { return pix >= _start && pix < _start + _len; }
|
||||
|
||||
static inline std::vector<LEDType> getLEDTypes() { return {{TYPE_NONE, "", PSTR("None")}}; } // not used. just for reference for derived classes
|
||||
static constexpr size_t getNumberOfPins(uint8_t type) { return isVirtual(type) ? 4 : isPWM(type) ? numPWMPins(type) : isHub75(type) ? 5 : is2Pin(type) + 1; } // credit @PaoloTK; for HUB75 the 5 slots store config params (panelW, panelH, chain, rows, cols), not GPIO pins
|
||||
static constexpr size_t getNumberOfPins(uint8_t type) { return isVirtual(type) ? 4 : isPWM(type) ? numPWMPins(type) : isHub75(type) ? 5 : is2Pin(type) + 1; } // credit @PaoloTK
|
||||
static constexpr size_t getNumberOfChannels(uint8_t type) { return hasWhite(type) + 3*hasRGB(type) + hasCCT(type); }
|
||||
static constexpr bool hasRGB(uint8_t type) {
|
||||
return !((type >= TYPE_WS2812_1CH && type <= TYPE_WS2812_WWA) || type == TYPE_ANALOG_1CH || type == TYPE_ANALOG_2CH || type == TYPE_ONOFF);
|
||||
|
||||
+11
-21
@@ -30,10 +30,8 @@ static constexpr bool validatePinsAndTypes(const unsigned* types, unsigned numTy
|
||||
// Pins provided < pins required -> always invalid
|
||||
// Pins provided = pins required -> always valid
|
||||
// Pins provided > pins required -> valid if excess pins are a product of last type pins since it will be repeated
|
||||
// HUB75 types use their pin slots for config params, not GPIO - skip GPIO pin validation for them
|
||||
return Bus::isHub75(types[numTypes-1]) ? true :
|
||||
(sumPinsRequired(types, numTypes) > numPins) ? false :
|
||||
(numPins - sumPinsRequired(types, numTypes)) % Bus::getNumberOfPins(types[numTypes-1]) == 0;
|
||||
return (sumPinsRequired(types, numTypes) > numPins) ? false :
|
||||
(numPins - sumPinsRequired(types, numTypes)) % Bus::getNumberOfPins(types[numTypes-1]) == 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -49,7 +47,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) {
|
||||
//int rev_major = doc["rev"][0]; // 1
|
||||
//int rev_minor = doc["rev"][1]; // 0
|
||||
|
||||
long vid = doc[F("vid")] | VERSION; // 2605010 note: "vid" can be used to detect an update from older versions but only on first call, it is written to the new VID after buses are initialized
|
||||
//long vid = doc[F("vid")]; // 2010020
|
||||
|
||||
JsonObject id = doc["id"];
|
||||
getStringFromJson(cmDNS, id[F("mdns")], 33);
|
||||
@@ -86,7 +84,8 @@ bool deserializeConfig(JsonObject doc, bool fromFS) {
|
||||
JsonArray nw_ins = nw["ins"];
|
||||
if (!nw_ins.isNull()) {
|
||||
// as password are stored separately in wsec.json when reading configuration vector resize happens there, but for dynamic config we need to resize if necessary
|
||||
if (nw_ins.size() > 1 && nw_ins.size() > multiWiFi.size()) multiWiFi.resize(nw_ins.size()); // resize constructs objects while resizing
|
||||
size_t newSize = min((size_t)WLED_MAX_WIFI_COUNT, nw_ins.size()); // cap at WLED_MAX_WIFI_COUNT (prevent oversizing when too many Wi-Fi entries)
|
||||
if (nw_ins.size() > 1 && newSize > multiWiFi.size()) multiWiFi.resize(newSize); // resize constructs objects while resizing
|
||||
for (JsonObject wifi : nw_ins) {
|
||||
JsonArray ip = wifi["ip"];
|
||||
JsonArray gw = wifi["gw"];
|
||||
@@ -695,18 +694,8 @@ bool deserializeConfig(JsonObject doc, bool fromFS) {
|
||||
JsonArray timersArray = tm["ins"];
|
||||
if (!timersArray.isNull()) {
|
||||
clearTimers();
|
||||
bool legacySunriseLoaded = false; // migration flag: pre 16.0 used hour=255 for both sunrise & sunset (type determined by array position)
|
||||
for (JsonObject timer : timersArray) {
|
||||
uint8_t h = timer[F("hour")] | 0;
|
||||
// legacy migration for pre 16.0 (vid < 2605010): first occurrence = sunrise, second occurrence = sunset
|
||||
if (vid < 2605010 && h == 255) {
|
||||
if (legacySunriseLoaded) {
|
||||
h = TH_SUNSET; // second "255" entry is actually sunset
|
||||
} else {
|
||||
legacySunriseLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
int8_t m = timer[F("min")] | 0;
|
||||
uint8_t p = timer[F("macro")] | 0;
|
||||
uint8_t dow = timer[F("dow")] | 127;
|
||||
@@ -802,14 +791,14 @@ void resetConfig() {
|
||||
}
|
||||
|
||||
bool deserializeConfigFromFS() {
|
||||
[[maybe_unused]] bool success = deserializeConfigSec();
|
||||
(void) deserializeConfigSec(); // success/failure is ignored intentionally. We need to read cfg.json even when wsec.json has errors.
|
||||
|
||||
if (!requestJSONBufferLock(JSON_LOCK_CFG_DES)) return false;
|
||||
|
||||
DEBUG_PRINTLN(F("Reading settings from /cfg.json..."));
|
||||
|
||||
success = readObjectFromFile(s_cfg_json, nullptr, pDoc);
|
||||
|
||||
bool success = readObjectFromFile(s_cfg_json, nullptr, pDoc);
|
||||
if (!success || (pDoc->overflowed())) pDoc->clear(); // corrupted/too-large → same as missing: seed defaults
|
||||
// NOTE: This routine deserializes *and* applies the configuration
|
||||
// Therefore, must also initialize ethernet from this function
|
||||
JsonObject root = pDoc->as<JsonObject>();
|
||||
@@ -1275,7 +1264,7 @@ bool deserializeConfigSec() {
|
||||
if (!requestJSONBufferLock(JSON_LOCK_CFG_SEC_DES)) return false;
|
||||
|
||||
bool success = readObjectFromFile(s_wsec_json, nullptr, pDoc);
|
||||
if (!success) {
|
||||
if (!success || pDoc->overflowed() || pDoc->size() < 1) { // reject if overflowed (noMemory) or empty
|
||||
releaseJSONBufferLock();
|
||||
return false;
|
||||
}
|
||||
@@ -1285,7 +1274,8 @@ bool deserializeConfigSec() {
|
||||
size_t n = 0;
|
||||
JsonArray nw_ins = root["nw"]["ins"];
|
||||
if (!nw_ins.isNull()) {
|
||||
if (nw_ins.size() > 1 && nw_ins.size() > multiWiFi.size()) multiWiFi.resize(nw_ins.size()); // resize constructs objects while resizing
|
||||
size_t newSize = min((size_t)WLED_MAX_WIFI_COUNT, nw_ins.size()); // cap at WLED_MAX_WIFI_COUNT (prevent oversizing when too many Wi-Fi entries)
|
||||
if (nw_ins.size() > 1 && newSize > multiWiFi.size()) multiWiFi.resize(newSize); // resize constructs objects while resizing
|
||||
for (JsonObject wifi : nw_ins) {
|
||||
char pw[65] = "";
|
||||
getStringFromJson(pw, wifi["psk"], 65);
|
||||
|
||||
+1
-13
@@ -269,10 +269,7 @@ void loadCustomPalettes() {
|
||||
char fileName[32];
|
||||
sprintf_P(fileName, PSTR("/palette%d.json"), index);
|
||||
if (WLED_FS.exists(fileName)) {
|
||||
// add gray placeholders to preserve palette IDs for subsequent slots (is omitted in UI but shown in cpal.htm)
|
||||
for (unsigned g = 0; g < emptyPaletteGap; g++)
|
||||
customPalettes.push_back(CRGBPalette16(CRGB(128, 128, 128)));
|
||||
emptyPaletteGap = 0; // reset gap counter if file exists
|
||||
emptyPaletteGap = 0; // reset gap counter if file exists
|
||||
DEBUGFX_PRINTF_P(PSTR("Reading palette from %s\n"), fileName);
|
||||
if (readObjectFromFile(fileName, nullptr, &pDoc)) {
|
||||
JsonArray pal = pDoc[F("palette")];
|
||||
@@ -312,15 +309,6 @@ void loadCustomPalettes() {
|
||||
}
|
||||
}
|
||||
|
||||
size_t removeUsermodPalettes(const char *name) {
|
||||
size_t before = usermodPalettes.size();
|
||||
for (int i = usermodPalettes.size() - 1; i >= 0; i--) {
|
||||
if (usermodPalettes[i].name == name)
|
||||
usermodPalettes.erase(usermodPalettes.begin() + i);
|
||||
}
|
||||
return before - usermodPalettes.size();
|
||||
}
|
||||
|
||||
// convert HSV (16bit hue) to RGB (32bit with white = 0), optimized for speed
|
||||
WLED_O2_ATTR void hsv2rgb_spectrum(const CHSV32& hsv, CRGBW& rgb) {
|
||||
unsigned p, q, t;
|
||||
|
||||
+1
-12
@@ -61,20 +61,9 @@ void adjust_color(CRGBW& rgb, int32_t hueShift, int32_t satChange,int32_t valueC
|
||||
[[gnu::hot, gnu::pure]] uint32_t ColorFromPalette(const CRGBPalette16 &pal, unsigned index, uint8_t brightness = (uint8_t)255U, TBlendType blendType = LINEARBLEND);
|
||||
CRGBPalette16 generateHarmonicRandomPalette(const CRGBPalette16 &basepalette);
|
||||
CRGBPalette16 generateRandomPalette();
|
||||
// Palette registered by a usermod at fixed IDs (255, 254, 253... 201).
|
||||
// Display name is "name: palName" (if palName non-null) or falls back to "name index" (e.g. "AudioReactive 1"), see util.cpp
|
||||
struct UsermodPalette {
|
||||
CRGBPalette16 palette;
|
||||
const char *name; // PROGMEM base name string (must not be nullptr), this name is used in removeUsermodPalettes()
|
||||
uint8_t palIndex; // index of the palette for a usermod
|
||||
const char *palName; // optional PROGMEM display name; if set, shown as "name: palName" (e.g. "AudioReactive: Audio Responsive Hue"), otherwise falls back to "name index"
|
||||
};
|
||||
|
||||
void loadCustomPalettes();
|
||||
size_t removeUsermodPalettes(const char *name); // remove all entries from usermodPalettes whose name pointer matches 'name'
|
||||
extern std::vector<CRGBPalette16> customPalettes;
|
||||
extern std::vector<UsermodPalette> usermodPalettes;
|
||||
inline size_t getPaletteCount() { return FIXED_PALETTE_COUNT + usermodPalettes.size() + customPalettes.size(); }
|
||||
inline size_t getPaletteCount() { return FIXED_PALETTE_COUNT + customPalettes.size(); }
|
||||
|
||||
void hsv2rgb_spectrum(const CHSV32& hsv, CRGBW& rgb);
|
||||
void hsv2rgb_spectrum(const CHSV& hsv, CRGB& rgb);
|
||||
|
||||
+2
-14
@@ -10,16 +10,8 @@ constexpr size_t FASTLED_PALETTE_COUNT = 7; // 6-12 = sizeof(fastledPalettes)
|
||||
constexpr size_t GRADIENT_PALETTE_COUNT = 59; // 13-72 = sizeof(gGradientPalettes) / sizeof(gGradientPalettes[0]);
|
||||
constexpr size_t DYNAMIC_PALETTE_COUNT = 6; // 0- 5 = dynamic palettes (0=default(virtual),1=random,2=primary,3=primary+secondary,4=primary+secondary+tertiary,5=primary+secondary(+tertiary if not black)
|
||||
constexpr size_t FIXED_PALETTE_COUNT = DYNAMIC_PALETTE_COUNT + FASTLED_PALETTE_COUNT + GRADIENT_PALETTE_COUNT; // total number of fixed palettes
|
||||
|
||||
// Palette ID space layout (palette IDs are uint8_t, 0-255):
|
||||
// 0 .. FIXED_PALETTE_COUNT-1 : fixed built-in palettes
|
||||
// 72 .. WLED_CUSTOM_PALETTE_ID_BASE(200) : user custom palettes (index 0 = ID 200, growing downward)
|
||||
// 201.. WLED_USERMOD_PALETTE_ID_BASE(255): usermod-registered palettes (index 0 = ID 255, growing downward)
|
||||
constexpr uint8_t WLED_USERMOD_PALETTE_ID_BASE = 255; // highest ID for usermod palettes
|
||||
constexpr uint8_t WLED_CUSTOM_PALETTE_ID_BASE = 200; // highest ID for user custom palettes
|
||||
constexpr size_t WLED_MAX_USERMOD_PALETTES = WLED_USERMOD_PALETTE_ID_BASE - WLED_CUSTOM_PALETTE_ID_BASE; // 55 slots (IDs 201-255)
|
||||
#ifndef ESP8266
|
||||
#define WLED_MAX_CUSTOM_PALETTES (WLED_CUSTOM_PALETTE_ID_BASE - FIXED_PALETTE_COUNT + 1) // 129 slots (IDs 72-200)
|
||||
#define WLED_MAX_CUSTOM_PALETTES (255 - FIXED_PALETTE_COUNT) // allow up to 255 total palettes, user is warned about stability issues when adding more than 10
|
||||
#else
|
||||
#define WLED_MAX_CUSTOM_PALETTES 10 // ESP8266: limit custom palettes to 10
|
||||
#endif
|
||||
@@ -390,7 +382,7 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit");
|
||||
#define BTN_TYPE_TOUCH_SWITCH 9
|
||||
|
||||
//Ethernet board types
|
||||
#define WLED_NUM_ETH_TYPES 16
|
||||
#define WLED_NUM_ETH_TYPES 14
|
||||
|
||||
|
||||
#define WLED_ETH_NONE 0
|
||||
@@ -407,9 +399,6 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit");
|
||||
#define WLED_ETH_ESP32_POE_WROVER 11
|
||||
#define WLED_ETH_LILYGO_T_POE_PRO 12
|
||||
#define WLED_ETH_GLEDOPTO 13
|
||||
#define WLED_ETH_QUINLED_V4_UNOQUAD 14
|
||||
#define WLED_ETH_QUINLED_V4_OCTA 15
|
||||
|
||||
|
||||
//Hue error codes
|
||||
#define HUE_ERROR_INACTIVE 0
|
||||
@@ -492,7 +481,6 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit");
|
||||
#define JSON_LOCK_LEDGAP 20
|
||||
#define JSON_LOCK_LEDMAP_ENUM 21
|
||||
#define JSON_LOCK_REMOTE 22
|
||||
#define JSON_LOCK_OTA 23
|
||||
|
||||
// Timer mode types
|
||||
#define NL_MODE_SET 0 //After nightlight time elapsed, set to target brightness
|
||||
|
||||
+1
-136
@@ -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; // upper nibble is reserved, lower nibble is sequence number, if set to 0 no sequence checking is done (if enabled)
|
||||
pkt[2] = 0x00; // reserved
|
||||
pkt[3] = 0x0B; // RGB, 8bit per channel
|
||||
pkt[4] = 0x01; // destination id (not used but 0x01 is default output)
|
||||
pkt[5] = (off >> 24) & 255; // DDP protocol 4-7 is offset
|
||||
@@ -222,138 +222,3 @@ function sendDDP(ws, start, len, colors) {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Pin utilities
|
||||
function getOwnerName(o,t,n) {
|
||||
// Use firmware-provided name if available
|
||||
if(n) return n;
|
||||
if(!o) return "System"; // no owner provided
|
||||
if(o===0x85){ return getBtnTypeName(t); } // button pin
|
||||
return "UM #"+o;
|
||||
}
|
||||
function getBtnTypeName(t) {
|
||||
var n=["None","Reserved","Push","Push Inv","Switch","PIR","Touch","Analog","Analog Inv","Touch Switch"];
|
||||
var label = n[t] || "?";
|
||||
return 'Button <span style="font-size:10px;color:#888">'+label+'</span>';
|
||||
}
|
||||
function getCaps(p,c) {
|
||||
var r=[];
|
||||
// Use touch info from settings endpoint
|
||||
if(d.touch && d.touch.includes(p)) r.push("Touch");
|
||||
if(d.ro_gpio && d.ro_gpio.includes(p)) r.push("Input Only");
|
||||
// Use other caps from JSON (Analog, Boot, Input Only)
|
||||
if(c&0x02) r.push("Analog");
|
||||
if(c&0x08) r.push("Flash Boot");
|
||||
if(c&0x10) r.push("Bootstrap");
|
||||
return r.length?r.join(", "):"-";
|
||||
}
|
||||
|
||||
// Fetch GPIO caps (/settings/s.js?p=11) then pin occupancy (/json/pins) with retry.
|
||||
// Caches result in d.pinsData. Calls cb() when ready (or on failure).
|
||||
// If page already loaded its own s.js (d.max_gpio set), skips caps load and goes straight to pins fetch.
|
||||
function fetchPinInfo(cb, retries=5) {
|
||||
if (d.pinsData) { cb&&cb(); return; }
|
||||
var done=false, fr=retries;
|
||||
function doFetch() {
|
||||
fetch(getURL('/json/pins'))
|
||||
.then(r=>r.json())
|
||||
.then(j=>{ if(!done){done=true; d.pinsData=j.pins||[]; cb&&cb();} })
|
||||
.catch(()=>{ fr-->0 ? setTimeout(doFetch,100) : (!done&&(done=true,d.pinsData=[],cb&&cb())); });
|
||||
}
|
||||
if (d.max_gpio) { doFetch(); return; }
|
||||
// Load GPIO caps from s.js?p=11 first (sets d.rsvd/ro_gpio/max_gpio/touch/adc/um_p)
|
||||
d.max_gpio=50; d.rsvd=[]; d.ro_gpio=[]; d.touch=[]; d.adc=[]; d.um_p=[];
|
||||
var cr=retries;
|
||||
function tryCaps() {
|
||||
var s=cE("script"); s.src=getURL('/settings/s.js?p=11');
|
||||
d.body.appendChild(s);
|
||||
s.onload=function(){ GetV(); doFetch(); };
|
||||
s.onerror=function(){ cr-->0 ? setTimeout(tryCaps,100) : doFetch(); };
|
||||
}
|
||||
tryCaps();
|
||||
}
|
||||
|
||||
// Pin dropdown utilities
|
||||
// Create or rebuild a pin <select> from an <input> or existing <select>
|
||||
// name: form field name, requirement flags bitmask: 1=output, 2=touch, 4=ADC
|
||||
function makePinSelect(name, flags) {
|
||||
let el = gN(name);
|
||||
if (!el) return null;
|
||||
let v = parseInt(el.value);
|
||||
if (isNaN(v)) v = -1;
|
||||
|
||||
let sel;
|
||||
if (el.tagName === "SELECT") {
|
||||
sel = el;
|
||||
while (sel.lastChild) sel.lastChild.remove();
|
||||
} else {
|
||||
sel = cE('select');
|
||||
sel.classList.add("pin");
|
||||
sel.name = el.name;
|
||||
if (el.required) sel.required = true;
|
||||
let oc = el.getAttribute("onchange");
|
||||
if (oc) sel.setAttribute("onchange", oc);
|
||||
el.parentElement.replaceChild(sel, el);
|
||||
}
|
||||
|
||||
let hasV = false;
|
||||
for (let j = -1; j < (d.max_gpio||0); j++) {
|
||||
if (j > -1 && d.rsvd && d.rsvd.includes(j)) continue;
|
||||
if (j > -1 && (flags & 1) && d.ro_gpio && d.ro_gpio.includes(j)) continue;
|
||||
if (j > -1 && (flags & 2) && (!d.touch || !d.touch.includes(j))) continue;
|
||||
if (j > -1 && (flags & 4) && (!d.adc || !d.adc.includes(j))) continue;
|
||||
|
||||
let pInfo = d.pinsData && d.pinsData.find(p => p.p === j);
|
||||
let used = j > -1 && pInfo && pInfo.a && j !== v;
|
||||
let txt = j === -1 ? "unused" : `${j}`;
|
||||
if (used) txt += ` (${getOwnerName(pInfo.o, pInfo.t, pInfo.n)})`;
|
||||
// if (j > -1 && d.ro_gpio && d.ro_gpio.includes(j)) txt += " (R/O)"; // read only pins note: removed as pin is not shown for outputs
|
||||
|
||||
let opt = cE("option");
|
||||
opt.value = j;
|
||||
opt.text = txt;
|
||||
sel.appendChild(opt);
|
||||
|
||||
if (j === v) { opt.selected = true; hasV = true; }
|
||||
else if (used) opt.disabled = true;
|
||||
}
|
||||
|
||||
// Safety for invalid pins currently saved
|
||||
if (!hasV && v >= 0) {
|
||||
let opt = cE("option");
|
||||
opt.value = v; opt.text = v + " ⚠"; opt.selected = true;
|
||||
sel.insertBefore(opt, sel.options[1]);
|
||||
}
|
||||
sel.dataset.val = v;
|
||||
return sel;
|
||||
}
|
||||
|
||||
// Convert pin <select> back to <input type="number">
|
||||
function unmakePinSelect(name) {
|
||||
let sel = gN(name);
|
||||
if (!sel || sel.tagName !== "SELECT") return null;
|
||||
let inp = cE('input');
|
||||
inp.type = "number";
|
||||
inp.name = sel.name;
|
||||
inp.value = sel.value;
|
||||
inp.className = "s";
|
||||
if (sel.required) inp.required = true;
|
||||
let oc = sel.getAttribute("onchange");
|
||||
if (oc) inp.setAttribute("onchange", oc);
|
||||
sel.parentElement.replaceChild(inp, sel);
|
||||
return inp;
|
||||
}
|
||||
// Add option to select, auto-select matching data-val
|
||||
function addOption(sel, txt, val) {
|
||||
if (!sel) return null;
|
||||
let opt = cE("option");
|
||||
opt.value = val;
|
||||
opt.text = txt;
|
||||
sel.appendChild(opt);
|
||||
if (sel.dataset.val !== undefined) {
|
||||
for (let i = 0; i < sel.options.length; i++) {
|
||||
if (sel.options[i].value == sel.dataset.val) { sel.selectedIndex = i; break; }
|
||||
}
|
||||
}
|
||||
return opt;
|
||||
}
|
||||
|
||||
+23
-19
@@ -5,7 +5,7 @@
|
||||
<title>WLED Palette Editor</title>
|
||||
<!--* <link rel="stylesheet" href="style.css">
|
||||
<script src="common.js"></script>
|
||||
<script src="iro.js"></script>
|
||||
<script src="iro.js"></script>
|
||||
<link rel="icon" href="data:,"> *-->
|
||||
</head>
|
||||
<body>
|
||||
@@ -71,8 +71,6 @@
|
||||
let copyColor = '#000';
|
||||
let ws = null;
|
||||
let maxCol; // max colors to send out in one chunk, ESP8266 is limited to ~50 (500 bytes), ESP32 can do ~128 (1340 bytes)
|
||||
let _applySeq = 0; // incremented each time applyLED fires; used to cancel stale in-flight previews
|
||||
let _httpQueue = [], _httpRun = 0;
|
||||
|
||||
// load external resources in sequence to avoid 503 errors if heap is low, repeats indefinitely until loaded
|
||||
(function loadFiles() {
|
||||
@@ -331,7 +329,17 @@
|
||||
});
|
||||
draw();
|
||||
}
|
||||
/*
|
||||
function rndPal() {
|
||||
gr.innerHTML = '';
|
||||
addMk(0, rndHex(), 1);
|
||||
const cnt = Math.floor(Math.random() * 3) + 2, pos = new Set();
|
||||
while (pos.size < cnt) pos.add(Math.floor(Math.random() * 254) + 1);
|
||||
[...pos].sort((a,b)=>a-b).forEach(t => addMk(t, rndHex()));
|
||||
addMk(255, rndHex(), 1);
|
||||
}*/
|
||||
|
||||
|
||||
// convert hsl to hex using canvas
|
||||
function hslToHex(h, s, l) {
|
||||
let ctx = cE("canvas").getContext("2d");
|
||||
@@ -460,11 +468,7 @@
|
||||
rm.className = 'sml';
|
||||
rm.title = 'Delete palette';
|
||||
rm.innerHTML = '✖';
|
||||
rm.onclick = () => {
|
||||
requestJson({rmcpal:i}); // send remove command
|
||||
setTimeout(refr, 500); // slight delay to allow ESP to process deletion before fetching updated list
|
||||
localStorage.removeItem('wledPalx'); // invalidate main UI cache
|
||||
};
|
||||
rm.onclick = () => { requestJson({rmcpal:i}); setTimeout(refr, 500); };
|
||||
|
||||
const name = isEmpty(p.palette) ? 'Empty slot' : 'Custom' + i;
|
||||
const css = isEmpty(p.palette) ? '#666' : cssArr(p.palette);
|
||||
@@ -588,7 +592,7 @@
|
||||
// download external palettes, these were hand picked from cpt-city (http://seaviewsensing.com/pub/cpt-city/)
|
||||
// all palettes are licensed "free to use", converted to WLED JSON format by @dedehai
|
||||
function fetchExt() {
|
||||
fetch('https://wled.github.io/wled-web-tools/cpt_city_selection.json')
|
||||
fetch('https://dedehai.github.io/cpt_city_selection.json')
|
||||
.then(r => { if (!r.ok) throw new Error(); return r.json(); })
|
||||
.then(data => {
|
||||
try { localStorage.setItem('wledCptCityJson', JSON.stringify(data)); } catch(e) {}
|
||||
@@ -617,16 +621,21 @@
|
||||
|
||||
async function requestJson(cmd)
|
||||
{
|
||||
if (ws && ws.readyState == 1 && ws.bufferedAmount < 32768) {
|
||||
if (ws && ws.readyState == 1) {
|
||||
try {
|
||||
ws.send(JSON.stringify(cmd));
|
||||
await new Promise(r => setTimeout(r, 15)); // short delay to give ESP time to process (fewer packets dropped)
|
||||
return 1;
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// HTTP fallback
|
||||
if (_httpQueue.length >= 5) return -1; // queue full; applyLED cancels stale queues before sending
|
||||
if (!window._httpQueue) {
|
||||
window._httpQueue = [];
|
||||
window._httpRun = 0;
|
||||
}
|
||||
if (_httpQueue.length >= 5) {
|
||||
return Promise.resolve(-1); // reject if too many queued requests
|
||||
}
|
||||
|
||||
return new Promise(resolve => {
|
||||
_httpQueue.push({ cmd, resolve });
|
||||
(async function run() {
|
||||
@@ -641,7 +650,7 @@
|
||||
cache: 'no-store'
|
||||
});
|
||||
} catch (e) {}
|
||||
await new Promise(r => setTimeout(r, 120)); // delay between requests (go slow, this is the http fallback if WS fails)
|
||||
await new Promise(r => setTimeout(r, 120));
|
||||
q.resolve(0);
|
||||
}
|
||||
_httpRun = 0;
|
||||
@@ -653,12 +662,8 @@
|
||||
async function applyLED()
|
||||
{
|
||||
if (!palCache.length) return;
|
||||
const seq = ++_applySeq;
|
||||
// discard pending HTTP chunks from any previous preview so stale data doesn't drain slowly
|
||||
while (_httpQueue.length) _httpQueue.shift().resolve(-1); // resolve dropped entries so their awaiters can observe the seq change and exit
|
||||
try {
|
||||
let st = await (await fetch(getURL('/json/state'), { cache: 'no-store' })).json();
|
||||
if (seq !== _applySeq) return; // superseded by a newer preview request
|
||||
if (!st.seg || !st.seg.length) return;
|
||||
|
||||
// get selected segments, use main segment if none selected
|
||||
@@ -675,7 +680,6 @@
|
||||
arr.push(palCache[len > 1 ? Math.round(i * 255 / (len - 1)) : 0]);
|
||||
// send colors in chunks
|
||||
for (let j = 0; j < arr.length; j += maxCol) {
|
||||
if (seq !== _applySeq) return; // superseded mid-send
|
||||
let chunk = [s.start + j, ...arr.slice(j, j + maxCol)];
|
||||
await requestJson({ seg: { id: s.id, i: chunk } });
|
||||
}
|
||||
|
||||
@@ -147,7 +147,7 @@ p {
|
||||
font-size: 16px;
|
||||
}
|
||||
.fs1 {
|
||||
font-size: 32px;
|
||||
font-size: 48px;
|
||||
}
|
||||
.fs2 {
|
||||
font-size: 28px;
|
||||
|
||||
@@ -9,24 +9,10 @@
|
||||
<link rel="stylesheet" href="style.css"></head>
|
||||
<body>
|
||||
<div class="bgc1 clearfix">
|
||||
<h1 class="mhmm mvm"><span class="fgc1">Font Name:</span> wled122 <small class="fgc1">(Glyphs: 26)</small></h1>
|
||||
<h1 class="mhmm mvm"><span class="fgc1">Font Name:</span> wled122 <small class="fgc1">(Glyphs: 25)</small></h1>
|
||||
</div>
|
||||
<div class="clearfix mhl ptl">
|
||||
<h1 class="mvm mtn fgc1">Grid Size: 16</h1>
|
||||
<div class="glyph fs1">
|
||||
<div class="clearfix bshadow0 pbs">
|
||||
<span class="i-search"></span>
|
||||
<span class="mls"> i-search</span>
|
||||
</div>
|
||||
<fieldset class="fs0 size1of1 clearfix hidden-false">
|
||||
<input type="text" readonly value="e0a1" class="unit size1of2" />
|
||||
<input type="text" maxlength="1" readonly value="" class="unitRight size1of2 talign-right" />
|
||||
</fieldset>
|
||||
<div class="fs0 bshadow0 clearfix hidden-true">
|
||||
<span class="unit pvs fgc1">liga: </span>
|
||||
<input type="text" readonly value="search, magnifier" class="liga unitRight" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="glyph fs1">
|
||||
<div class="clearfix bshadow0 pbs">
|
||||
<span class="i-pixelforge"></span>
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
<glyph unicode="" glyph-name="presets" d="M704 704c131.413 0 234.667-103.253 234.667-234.667 0-161.28-145.067-292.693-364.8-491.946l-61.867-56.32-61.867 55.893c-219.733 199.68-364.8 331.093-364.8 492.373 0 131.414 103.254 234.667 234.667 234.667 74.24 0 145.493-34.56 192-89.173 46.507 54.613 117.76 89.173 192 89.173zM516.267 40.533c203.093 183.894 337.066 305.494 337.066 428.8 0 85.334-64 149.334-149.333 149.334-65.707 0-129.707-42.24-151.893-100.694h-79.787c-22.613 58.454-86.613 100.694-152.32 100.694-85.333 0-149.333-64-149.333-149.334 0-123.306 133.973-244.906 337.066-428.8l4.267-4.266z" />
|
||||
<glyph unicode="" glyph-name="info" d="M512 746.667c235.52 0 426.667-191.147 426.667-426.667s-191.147-426.667-426.667-426.667-426.667 191.147-426.667 426.667 191.147 426.667 426.667 426.667zM554.667 106.667v256h-85.334v-256h85.334zM554.667 448v85.333h-85.334v-85.333h85.334z" />
|
||||
<glyph unicode="" glyph-name="power" d="M554.667 704v-426.667h-85.334v426.667h85.334zM760.747 611.413c82.773-70.4 135.253-174.506 135.253-291.413 0-212.053-171.947-384-384-384s-384 171.947-384 384c0 116.907 52.48 221.013 135.253 291.413l60.16-60.16c-66.986-54.613-110.080-137.813-110.080-231.253 0-165.12 133.547-298.667 298.667-298.667s298.667 133.547 298.667 298.667c0 93.44-43.094 176.64-110.507 230.827z" />
|
||||
<glyph unicode="" glyph-name="search" d="M902.213 27.99l-197.073 167.615c-20.373 18.336-42.161 26.752-59.761 25.941 46.52 54.492 74.622 125.188 74.622 202.455 0 172.313-139.687 312-312 312-172.311 0-312-139.687-312-312s139.687-312 312-312c77.266 0 147.962 28.1 202.455 74.624-0.812-17.6 7.605-39.388 25.941-59.761l167.615-197.073c28.698-31.887 75.58-34.574 104.178-5.976s25.913 75.48-5.974 104.178zM408 216c-114.874 0-208 93.125-208 208s93.125 208 208 208 208-93.125 208-208-93.125-208-208-208z" />
|
||||
<glyph unicode="" glyph-name="settings" d="M816.64 280.064l85.504-67.584c8.192-6.144 10.24-16.896 5.12-26.112l-81.92-141.824c-5.12-9.216-15.872-12.8-25.088-9.216l-101.888 40.96c-20.992-15.872-44.032-29.696-69.12-39.936l-15.36-108.544c-1.024-10.24-9.728-17.408-19.968-17.408h-163.84c-10.24 0-18.432 7.168-20.48 17.408l-15.36 108.544c-25.088 10.24-47.616 23.552-69.12 39.936l-101.888-40.96c-9.216-3.072-19.968 0-25.088 9.216l-81.92 141.824c-4.608 8.704-2.56 19.968 5.12 26.112l86.528 67.584c-2.048 12.8-3.072 26.624-3.072 39.936s1.536 27.136 3.584 39.936l-86.528 67.584c-8.192 6.144-10.24 16.896-5.12 26.112l81.92 141.824c5.12 9.216 15.872 12.8 25.088 9.216l101.888-40.96c20.992 15.872 44.032 29.696 69.12 39.936l15.36 108.544c1.536 10.24 9.728 17.408 19.968 17.408h163.84c10.24 0 18.944-7.168 20.48-17.408l15.36-108.544c25.088-10.24 47.616-23.552 69.12-39.936l101.888 40.96c9.216 3.072 19.968 0 25.088-9.216l81.92-141.824c4.608-8.704 2.56-19.968-5.12-26.112l-86.528-67.584c2.048-12.8 3.072-26.112 3.072-39.936s-1.024-27.136-2.56-39.936zM512 166.4c84.48 0 153.6 69.12 153.6 153.6s-69.12 153.6-153.6 153.6-153.6-69.12-153.6-153.6 69.12-153.6 153.6-153.6z" />
|
||||
<glyph unicode="" glyph-name="eye" d="M512 640c213.333 0 395.52-132.693 469.333-320-73.813-187.307-256-320-469.333-320s-395.52 132.693-469.333 320c73.813 187.307 256 320 469.333 320zM512 106.667c117.76 0 213.333 95.573 213.333 213.333s-95.573 213.333-213.333 213.333-213.333-95.573-213.333-213.333 95.573-213.333 213.333-213.333zM512 448c70.827 0 128-57.173 128-128s-57.173-128-128-128-128 57.173-128 128 57.173 128 128 128z" />
|
||||
<glyph unicode="" glyph-name="sync" d="M512 661.333c188.587 0 341.333-152.746 341.333-341.333 0-66.987-19.626-129.28-52.906-181.76l-62.294 62.293c19.2 35.414 29.867 76.374 29.867 119.467 0 141.227-114.773 256-256 256v-128l-170.667 170.667 170.667 170.666v-128zM512 64v128l170.667-170.667-170.667-170.666v128c-188.587 0-341.333 152.746-341.333 341.333 0 66.987 19.626 129.28 52.906 181.76l62.294-62.293c-19.2-35.414-29.867-76.374-29.867-119.467 0-141.227 114.773-256 256-256z" />
|
||||
|
||||
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -1,9 +1,9 @@
|
||||
@font-face {
|
||||
font-family: 'wled122';
|
||||
src:
|
||||
url('fonts/wled122.ttf?2tjc6') format('truetype'),
|
||||
url('fonts/wled122.woff?2tjc6') format('woff'),
|
||||
url('fonts/wled122.svg?2tjc6#wled122') format('svg');
|
||||
url('fonts/wled122.ttf?yzxblb') format('truetype'),
|
||||
url('fonts/wled122.woff?yzxblb') format('woff'),
|
||||
url('fonts/wled122.svg?yzxblb#wled122') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
@@ -24,9 +24,6 @@
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.i-search:before {
|
||||
content: "\e0a1";
|
||||
}
|
||||
.i-pixelforge:before {
|
||||
content: "\e900";
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@font-face {
|
||||
font-family: "WIcons";
|
||||
src: url(data:font/woff2;charset=utf-8;base64,d09GMgABAAAAAAtwAA0AAAAAFzwAAAsYAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP0ZGVE0cGhgGYACEHhEICp8QmBQLQAABNgIkA3wEIAWDGweCQBueEhEVqmUn+JmQuYW8aYRI8pyRytrpySK/jJBk9v+nbf19MKDMoI0Jwi77VwXMwsTIUSbEKvy5Veevw/8NG9XGRiobEQNstPsjEh74P+bfFxu1zQc2m+Aa+1eIVHCCRR1PoA82/wK/+v91bBjrxhAh4ktd/2kus/ntTjFbOEASKufInVMvm8JMpgyzruVsD5AssAcF1PIhpKsOUGhW6IGMOOvPDZlh7zJjzjrI26wbrOpZbwBAVfDQuPpH/fDP0TubwXIgpEa+JEjFsKsLM0KLQAQE14LrALAQORH0B0lBAopZoRo0lge+QsIkt7t+gH1ODaQKTBViUo7GA2pIRP8bMf8CyVQwgRDVMu155fOe5wufb3n+5YugFxkvrC9jXua83PHy4MuLo6+NJo1WjC4aXf5K8sr3K+Q2AIDnqTcbt7CFHniq7lfKm8oR5WblLABlM+L3CD7iA8VLxT0AxURRLf9RPiq/DiA/kPvLpQUwrWTSrYAbx6aq0GK5CHxuIQmcHz2lvxChFCIiUFFHKRJF8CoU/zMIYJApEPQffEglRnqtOgL79YqvGCYRhokEXIhkfaAnREw0kRC6CCNxkuxFQzw93OKS1xQKDBMlauQ1UrlcGhaKYZjEUyJRSqViQtRxRbSQ1FPtK/eKlmJsMY5hKgXG5TMXt6G2rDgyAMcjgoMR7poV+WDcbVxqvTsDvAORS+QQM2wZw3diBoGL38dToxuyzsk4nYkHkCVu770sO2Fz5JxyEU0u4z1XZVFkceJWXocPaTOA+92Io/YOLXvTRjscz0HAUTw/CLcyTucgZGFdrosTzUJc0IhwmBFxIUHvjr52qVjv4YUrN7ri+rW113ngEvY7yq4QtoprrqSDyOKsnsOs2IGfciFL4gFn+VXcWnqZj9/HvBpXLrdaRu++oO3foumwpo7cjd2ALMq997KOEraU4Tsx64GTD9xOPzSHXr48kFq2LIhZdmycVzhuXaHidowEmWhkxcpgdsUaZFkbvGorAleulNEr7ZFQBGFbruK2D8tMtL58WSC1fDVwqwLbbK4CKQbOkQsT5J+h+bzToxWcZVyF5zrfbEuz34vbiCwRQ3czj+DW5GO3deuAUwzeyTjcaHMA929BJ7K43rSVXXFcNw8cr6tbuxVZYoecmSM4MCPAAk0h62CVTLUrrnbVpsJvmDE8haaEtPppMQK24RCfXnfKVQLUAy/Ht7py69c7YsiTzrGEyTjsSIGLTv2Ugi+m0KUCTszXkmLkqMCLzykJcKEUeQkJgReLCMA5BNkNdBZQqM90YRZnfcA7YMjsRKx5A0gVV+Zqy/DDG60nkeUUYTsB3HEpdJomq5Px4nk8hLqL46/V3PI1iOqb5TsvEXNoCvA+gyOGRQRLE722LHtdTSzEuazpx3jdQKGtlOucGKNpB9r47WaOh1XHgVstq+BNHM9IQb3tlJAnne14goWMhJH+i7BmugtHUyiGXWmT0cu5OSANzUJCYD5TUGfV5qgybt8d2ptjQRNvkoyxHvBgBjy0U3Vsk5EmGwnAV20Zw46YwcwQ38CaOeKMHWq9mutb0IwI6BjVUp2ddFcP295j7qa66k0xbB3FkDRVy9Kd5touxjilliqq6Dl3au+49vURZaWX8eZYoN7q2qvjAjkH0R2y0Mz6TsrXxBkxwBed54FzzKFMHW2NTGejqZ5mWIahKJqaw6ywBkYxrJQKwkHWp1k6ahkno5fbgtmVlqA53LJAahePS607kGVlMNvvIrxs24EhPeV1lGUFElruEYTtDoBmRUWcO5BS19UMu9dsoRl6xRrnmhVw6lTC5s0/hEUnZq/a2TK1QF+YW1vucrEq1BfkaufOVdmJ0WE/JB0J6V1xUVF2k+n2FRWFXz9ge+bOjc2j6LwEOp0gidFOMOSxVcX2PPi6sclkcH9dXzWmSjVliqrMqK9neChRNQJrYn0xTR8w6D7N0KD56GfoFm40fbWQG1u26hMdqShSGHp7DWUmSb5xxSgUoyJ1n6zKNcYt/MoekjWjWD+tZ6nqYfV6Vl8wTV8c3UwUpE/k53d++613ZKQ5M//jmRTFKj9/5scGAInwr840R072vnlz53n2nIHKnvadXdtKzhD9ptX+JppBtmop95BUD6EKjbGptfPGv2rvjvpGHen9psduz22euz183owM+OfOaudtfcxiC5nCUCGOB0hodycVSuIn5TY05KovAu4+sjS4NGgW9fXXYYYJJwQkgs2Tll9cE6ib0LZa/LtO97t4ddsEHXZMLZVSXp6idgfuZSUPHjY1Da4vSQxv/PjpV9+qUvQ5BsO90RRuz6NkN0dr18R2G6C7f7fZfYqztKOYOTcE6ajlKbX2+u+T439nX3AH1FQVOyF+8gSP9Cq18eB1toItjLiuzUbxrJtjZUZFqbysZ6JhnEFd48l+0dxU09Brys839Zpqav6CjRAjgxyyyC9jhMRXGQCjvDhQmxpuD6dhgrQabZCmjysVs4fui/zjK0yVm7PSVOVwDxjCYJoEYg3r6MxMlYjyTFAMBSXrB33EwwergBt51BdGhYfRYeGGZ0SRAUoa2AWp2bKKqcbQ/EOGstEKsLxkK1SVqomTVF1RBcsSUKLSGmfHPHjwi4GpMre2FhczfAFj+CVwjoGUzo358yjz8uWVzx9XhNqhqJTRewZDjj6l2lYFHyd5T0lnZ4n614H7FFkUZgjrbXzytFgnFn/a1qarDKPCpR+aZ34oDS8K72ArdrzMNX0ZczW/HBhOrd6TyQ1/zHz2mj3OZJgXJ/SNXa6NG9mxadMvB6uqBODKrr7W2XlcDVFJRgPdjwYGhE51s4pqFIYp02xeCyJI0uChfBZZNaQuu9fc9W+uaJY22C59cruX1jNmkuyeptdP7/YRZsaCtemeaTcDyn17ZeW1qByao9275jJzl+4YHPaKyooP+lkdj1rKxVlUdJ5FFukzcvTe5vNQ8FrLIT2wg+Su9wXBHdX+1ZDSpyF/UXLmx94E9Lru5wr5wlWPTdg1F4G+i3Nn7e7ff38Nzt1+MY3bK1eBcUn9Iia2dJE/GUAyTa3gYUkJW03vYqnvva9uXWv7zbugvrmgoAYPq0j5MWA/3poasufX3zQxBH3lnzdTbbNzDclpZDVkvh758opB67FhwgnhTwydEWYPVbWpTXN4/uwRn9KB0v5hn3mzo49A+rLgx5/2qPb4LVny44+wvz+Smtq/bWtfH6Mi/OKgfvqxX9VvMo2oRiAn4J4LDeuGS0pUyv+kIz4Swy7FJWNuxXfjpooPdW3a/RvtQpHwjPaJdgVn+vw/rrjo/8rvSa6/ewdbfbJ/8yQw2NwGgrtZOCqMnqQnanm2tqWDAGsBIGWJKym6KTP0RAMwI+FgRhNHbTSJtNS/CdZpJCqQYkMOJW0qh9JiktoPV/fkRGhP1FClRpLdCCiqvQ0BwmoRD74QCVgUCVkeYax+uAhhphDHbFklLszd3/EoS8g7ASMBCyEhiyeMlQ4XCdImxMY+6Awu2TqY4S3duiRIlAhmbHd3JSReou4oNJuWqm03GfxIeUvA2xpaC6a/l8mmeN0E71BJECsNXL7sYUJsWhCy2KuYOgtzTr4TIG/XlhdY4x1mUSnTaYIqtrCiWHDPZKW9+aKyzgl/7/MoAAQUd6oEKIIgCEEYMCRCYuSBPJFkA/DIpMgLeSMf5Iv8kD8KQIFIhoJQMApBoSgMhSM5UnjIae98IFEfwn4XmrpvPULP5216LPpmNzJ0VXR2N7PD5G3zhL4UvaObbOiS7kvP5y2Ejm6OiiZiCjYBAAAA);
|
||||
src: url(data:font/woff2;charset=utf-8;base64,d09GMgABAAAAAAsAAA0AAAAAFlgAAAqqAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP0ZGVE0cGhgGYACEGhEICp08lnkLPgABNgIkA3gEIAWDGweCNhvxEVGUcFI3wBeFsYOmlCFXadeSCl4PGhMTwyMh0q9d2MXuDaeszCMkmT3Abd0Eu2ijAIMUa1IDbaQmRj/wndtnJB+d8BHN/+ZKv+zJJpUCCAsMA5IcArBbtlteAg6ToYi3nPp6KxH97fd9OQgssMYTSymghAPMMODmLNpvv/P8BPzeodosVKppyCRNZE0QEqlTCp0SqP9T4O4gAMzzFuTJg2RPa6/23s/f4IYKREKfr6tTc/cLu7dh2JTwmhJdUiSLQqZVQFvmy6mScazQAwlZ7apjDAOl7l8dYEyN5azo7xRYCTCz7gCAzIa7hoI38uBn9/NfQMIrA5RCyCOfOtya0oEneAKP2+M8AEzujgX5QIQYkXEhC5nk4BVC6f6L4cmN4YazURxLPmVQjD4XkFWhNcfmv38+EMNisJkOyOKfgx6n/2z9efLjZY9fPol6EvJEdaY7I5y1zu3Ok64kl58r7bcprplPfZ+GvELuPwEAiGmvZJPj8ErdT9kXF+1jV7AvsG3seNY31uuFw5m/LLgKwNzGLGd8mO+cfw6A8S5jCsM/9wfEH8iWrJEYBLUxMHfsLJpcHQqzOuDEFhQjM1otoVvVg94O/zMIoCJtI1ACwThSfr8yQL1KvQ5rAApCJOJJKBSl4cdB3IwhcY7A5i3/hNDuIJq7NmfVLJNq2Z1hACMTkEpSDwPzGMtL6Qj7EFl8BemVw4zAppSRHW5ZhSxVZIZwKIDXduoxP57T0cgYeukZbC1afoHHq6/OwUSERJEC0lcLGXjp0QKyd7tOLYzdaXLTFHYixavtddgQ0YyI6xbZbLleW+DKSDGxqrvjTWIRtNNgOF6yGYc0ZhihH0R1vR9WuWn2Q4pkWdcmW0QsbEIYzglYJKxhzbvPBSWhn9uiMsuraZ3jiQ75dBgpD4mW9tgSdSHFzLzEcnLiNDvb59n9lVxzrObWObWDviOG3Dwt5RZCKdLLyl04L0+xvKG6aEG0nJFTM6AcuXROdpzmFJCcH9+uWfmohYxDH0Nxk+nRN4ZT3uJW3O32b9GChl57lSFlYeur0F6s+ve/cC8GeUHLy5CeTZoB7XGeFaxDWspDQ9CBaXdnUZU9hGerGTqIgUtgQxhFauojOOdYXo78csyahwycYlRk/FVxQdrYrQc7r1tJQJv1+Xi5FbW+xPCwj5pLicU1YATAPRM9hVc9RfdxWc2300x9lIgM3K/9xgtYHI8miESYICECeMSQt3EtAdq7jhUlLE2CiYgNqUeZNrzc9nLTTg+EeckP9Kz28vnwTeoolOtCGyF5WOonuVZNPkHX/RKff2/l48rnCUbIfJZad59cYhSwkWPEJUQkRvZrYUMVbAKCS6jB/bp7M2ItABfEMpgBinhBFLgze5jkAlW62xjORdV67XqlRsPsObLU7cI+/4ss97HdGJ2iXMrTFMuRTzAe2SISYd9NlE6rZmS4ahqS+8GKTA7ZuWs9YGQfYGQHdUqbXcy+iQC2aiEDhkdLTkhvpoYOmp6tTc6yvgVbEIGdkoPu2sV275V27N23h7awKFxyUm5n1CGxXfscu7nrlINyF7v00vEyotuwG5If4LpYtazK+s3xmi4bpC2UoPNVnRa9JubCZj3+jg4Zl+iGnds38V2bNqxnXOKcUkYv8Vw1vppL4+lMDDMok9jqbFmxHE1LeYp/Sc6O03odj1droeRpqckiE7Qa4jB+nO5OlVyIymtCtJdACJKcTKe3Kct4DL+2knGWW/gpzKXr191XULH0Ay1NmD9ndUMvJaoqrCq5dStqFaosxPyr8/N902gfWD5BcFtmaqreo8wxq1T1+g6+d8iQDLnRJBeYZP4jf/MEBpHR0Lj1zmvSecXw/+vqjLhyTs+enLSoujoiRy3LDbIhvmtxCTAzTZPZBNzr683+Pi7U/TOZjE+Z8yHfzlQzMbsdS4t1ulIwTTJN6/hj5nBM5GevHDFhfTVob+tnthVHUVyu6o1q8GeQCn5TYowqQ4a0asLK33fsSX2zLCVo473WZ4XPWu/gTUr4n1nSfH38mHmqzKpYCucxNo9yXJO1toU1NYX8GuAm7EXRRVH9ja9f0zCPBxUQoNvXeb64MoLftWmu23d39+9eBU1d+UObPKOkpETCw4F7hvbO3brNG0u0Qnrt6B9fveVI0AIMu+aSkOtc3VrSJG5IwMsAv2Rwvfs6ObS5xyXIGfFGlW5cxjv/b4+s7/gTclsCLce7ZvXo6i3rJxi2P9ln4irW+XW89OtSmD3FBmYRo9jaDUvEEip98Bf1mytr7BaFwmJXXVf/AsfRQx8c8MBdywDCjkgAM7s2GDeXXEdyeRSPy4viEmSqzesYgTclp1nKvv50S/kNN+Me01EF0wbWprFZyoBXWACDKu3Cljz17p1WbIZ7xFwjnWai0bGQqsZQK2xf3jggsrSXIVaxQ5EaS2GoE0/jlHG6deccNaU4PqGWZWrG4+588wUzl9saGzWaiLzKjH1B/XEQ4LgwcYIvPn16iYkW1K9gpBLXayyhAJWUWWu2o4jXaVtbtfzXgfuQTk3DaPbaBw817l7OvamJX0Yz0gPOtn4jx9N79MYQbCTF84i+sxz6kXTj3MYcbvkx56XzGsMoWng/EOvWrcWLo2/Jki/by8srSCjHsse7du1fBqtFNQfTTAMOYnfw+6srmZgvttlWFUunU+SoXWtJpU4qtduaqVndnxftCHhw2c5Qs43pa9cbRfu0y1Nt5oTN6hPvfS+w6LgjvexcaoGJZO76IeYh02unz5FWVjqiKer9q78ieyU0Da60eLSoAM296/BJHbMKCIXs4Xs17vLgTs35ikkIrh9BLc4dTXAxNvU5UvV1Vb7bhkO4BhD/9lGHO+/fn4NjlwtHhQO8BSSK5a9HRtGUqfZwnbmeTb2rTbpb764lHTY8Ydt87VtQbHW9UlkZn5WaPRqobxB3qLN+/cb18J+f+dNROn5AISbO1lVAbseul2ewdd4vjwdVkzC2L02fKWdJE3fnxAH7JhVtSF4/EDxhQNoukP0c++bTOk5j6JfTPn6EbndfYOD6FcsJIgKUob1Inz6u5zRZLPsWD0IB4t4DWzCg1XLY/wIGg30NHTTauPsJKVtSOtJ9O2/rYgfF03zzHqybNYqD/yx4tforP6Ld9vAr2ybl/3yIRTcdrwzuetFFSSMAH0LMxI2+fkDdCcDYJyA3ipitETmBOLi8EZmJSOpOPFq/DTxMGhrE3JLs83kymayp5Uh8Ms2xDiHtOJqBLNjEdz8eyLwgrYDkpX6syTp5sNVEYdFEZesHeyLOS68ey57lZy682pmLOqOJ4wcS2GwSmTfZWPLgMWbCdumm7N32YP5QDH110k4bAfiCL0Df065NIHyl/q626c2Y16wHeIviHYE4G+iT5oGtK/bUXlddcGyeJwQBPKxxgIKM7PhKE0/2uuQ+juqSmmzG3PDQFXfqjwMpWpmyPLpjTQbA8zda3OddU9za9W/xDBTYht7SfiikklBMEosFGw5ceGBX1J+TRABBhBBGBFHEEIeCD40EkkghjYx77NI+y02QY4JeWJYom4tVXCrlMg1XCDMwWSeBQMpFORkyRSehUM1EmQphXMqVogyVtJNIKOEiERcruUyTmZOVJOkkzsrJEGRl5WR5AgA=);
|
||||
}
|
||||
|
||||
:root {
|
||||
|
||||
+19
-109
@@ -25,7 +25,6 @@ var pN = "", pI = 0, pNum = 0;
|
||||
var pmt = 1, pmtLS = 0;
|
||||
var lastinfo = {};
|
||||
var isM = false, mw = 0, mh=0;
|
||||
var bsOpts = null; // blending style options snapshot, used for dynamic filtering based on matrix mode (iOS compatibility)
|
||||
var ws, wsRpt=0;
|
||||
var cfg = {
|
||||
theme:{base:"dark", bg:{url:"", rnd: false, rndGrayscale: false, rndBlur: false}, alpha:{bg:0.6,tab:0.8}, color:{bg:""}},
|
||||
@@ -661,14 +660,12 @@ function parseInfo(i) {
|
||||
mw = i.leds.matrix ? i.leds.matrix.w : 0;
|
||||
mh = i.leds.matrix ? i.leds.matrix.h : 0;
|
||||
isM = mw>0 && mh>0;
|
||||
if (!bsOpts) bsOpts = Array.from(gId('bs').options).map(o => o.cloneNode(true)); // snapshot all options on first call
|
||||
const bsSel = gId('bs');
|
||||
// note: style.display='none' for option elements is not supported on all browsers (notably iOS)
|
||||
bsSel.replaceChildren(...bsOpts.filter(o => isM || o.dataset.type !== "2D").map(o => o.cloneNode(true))); // allow all in matrix mode, filter 2D blends otherwise
|
||||
if (!isM) {
|
||||
gId("filter2D").classList.add('hide'); // hide 2D effects in non-matrix mode
|
||||
gId("filter2D").classList.add('hide');
|
||||
gId('bs').querySelectorAll('option[data-type="2D"]').forEach((o,i)=>{o.style.display='none';});
|
||||
} else {
|
||||
gId("filter2D").classList.remove('hide');
|
||||
gId('bs').querySelectorAll('option[data-type="2D"]').forEach((o,i)=>{o.style.display='';});
|
||||
}
|
||||
gId("updBt").style.display = (i.opt & 1) ? '':'none';
|
||||
// if (i.noaudio) {
|
||||
@@ -989,39 +986,21 @@ function populatePalettes()
|
||||
);
|
||||
}
|
||||
gId('pallist').innerHTML=html;
|
||||
// append usermod palettes (fixed ID space: 255 down to 201)
|
||||
// append custom palettes (when loading for the 1st time)
|
||||
let li = lastinfo;
|
||||
if (!isEmpty(li) && li.umpalcount && li.umpalnames) {
|
||||
for (let j = 0; j < li.umpalcount; j++) {
|
||||
if (!isEmpty(li) && li.cpalcount) {
|
||||
for (let j = 0; j<li.cpalcount; j++) {
|
||||
let div = d.createElement("div");
|
||||
gId('pallist').appendChild(div);
|
||||
div.outerHTML = generateListItemHtml(
|
||||
'palette',
|
||||
255-j,
|
||||
li.umpalnames[j],
|
||||
'~ Custom '+j+' ~',
|
||||
'setPalette',
|
||||
`<div class="lstIprev" style="${genPalPrevCss(255-j)}"></div>`
|
||||
);
|
||||
}
|
||||
}
|
||||
// append user custom palettes (fixed ID space: 200 down to FIXED_PALETTE_COUNT+1)
|
||||
if (!isEmpty(li) && li.cpalcount) {
|
||||
for (let j = 0; j < li.cpalcount; j++) {
|
||||
const id = 200 - j;
|
||||
const pd = palettesData[id];
|
||||
if (pd && pd.length === 16 && pd.every(e => e[1] === 128 && e[2] === 128 && e[3] === 128)) continue; // skip gray gap-placeholder entries
|
||||
let div = d.createElement("div");
|
||||
gId('pallist').appendChild(div);
|
||||
div.outerHTML = generateListItemHtml(
|
||||
'palette',
|
||||
id,
|
||||
'~ Custom '+j+' ~',
|
||||
'setPalette',
|
||||
`<div class="lstIprev" style="${genPalPrevCss(id)}"></div>`
|
||||
);
|
||||
}
|
||||
}
|
||||
updateSelectedPalette(selectedPal); // update selection after adding usermod and custom palettes
|
||||
}
|
||||
|
||||
function redrawPalPrev()
|
||||
@@ -1478,9 +1457,7 @@ function readState(s,command=false)
|
||||
|
||||
tr = s.transition;
|
||||
gId('tt').value = tr/10;
|
||||
const bsSel = gId('bs');
|
||||
bsSel.value = s.bs || 0; // assign blending style
|
||||
if (!bsSel.value) bsSel.value = 0; // fall back to Fade if option does not exist
|
||||
gId('bs').value = s.bs || 0;
|
||||
if (tr===0) gId('bsp').classList.add('hide')
|
||||
else gId('bsp').classList.remove('hide')
|
||||
|
||||
@@ -2835,7 +2812,7 @@ function loadPalettesData() {
|
||||
if (lsPalData) {
|
||||
try {
|
||||
var d = JSON.parse(lsPalData);
|
||||
if (d && d.vid == lastinfo.vid && d.pcount == lastinfo.palcount) {
|
||||
if (d && d.vid == lastinfo.vid) {
|
||||
palettesData = d.p;
|
||||
redrawPalPrev();
|
||||
return resolve();
|
||||
@@ -2847,8 +2824,7 @@ function loadPalettesData() {
|
||||
getPalettesData(0, () => {
|
||||
localStorage.setItem("wledPalx", JSON.stringify({
|
||||
p: palettesData,
|
||||
vid: lastinfo.vid,
|
||||
pcount: lastinfo.palcount // total palette count, refresh cache if it changes
|
||||
vid: lastinfo.vid
|
||||
}));
|
||||
redrawPalPrev();
|
||||
setTimeout(resolve, 99); // delay optional
|
||||
@@ -3437,23 +3413,13 @@ function showVersionUpgradePrompt(info, oldVersion, newVersion) {
|
||||
|
||||
function reportUpgradeEvent(info, oldVersion, alwaysReport) {
|
||||
showToast('Reporting upgrade...');
|
||||
const IR_TYPES = {
|
||||
0: null, // not configured — omit field entirely
|
||||
1: "24-key", // white 24-key remote
|
||||
2: "24-key-ct", // white 24-key with CW, WW, CT+, CT- keys
|
||||
3: "40-key", // blue 40-key remote
|
||||
4: "44-key", // white 44-key remote
|
||||
5: "21-key", // white 21-key remote
|
||||
6: "6-key", // black 6-key learning remote
|
||||
7: "9-key", // 9-key remote
|
||||
8: "json-remote", // ir.json configurable remote
|
||||
};
|
||||
|
||||
// Reuse the info argument and fetch only /json/cfg (serialize requests to avoid 503s on low-heap devices)
|
||||
const infoData = info;
|
||||
fetch(getURL('/json/cfg'), {method: 'get'})
|
||||
.then(res => res.ok ? res.json() : Promise.reject(new Error('Failed to fetch /json/cfg')))
|
||||
.then(cfgData => {
|
||||
// Fetch fresh data from /json/info endpoint as requested
|
||||
fetch(getURL('/json/info'), {
|
||||
method: 'get'
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(infoData => {
|
||||
// Map to UpgradeEventRequest structure per OpenAPI spec
|
||||
// Required fields: deviceId, version, previousVersion, releaseName, chip, ledCount, isMatrix, bootloaderSHA256
|
||||
const upgradeData = {
|
||||
@@ -3468,58 +3434,13 @@ function reportUpgradeEvent(info, oldVersion, alwaysReport) {
|
||||
brand: infoData.brand, // Device brand (always present)
|
||||
product: infoData.product, // Product name (always present)
|
||||
flashSize: infoData.flash, // Flash size (always present)
|
||||
repo: infoData.repo, // GitHub repository (always present)
|
||||
fsUsed: infoData.fs?.u, // Filesystem used space in kB
|
||||
fsTotal: infoData.fs?.t, // Filesystem total space in kB
|
||||
|
||||
// LED hardware
|
||||
busCount: cfgData.hw?.led?.ins?.length ?? 1,
|
||||
busTypes: (cfgData.hw?.led?.ins ?? []).map(b => busTypeToString(b.type)),
|
||||
matrixWidth: infoData.leds?.matrix?.w,
|
||||
matrixHeight: infoData.leds?.matrix?.h,
|
||||
ledFeatures: [
|
||||
...(infoData.leds?.lc & 0x02 ? ["rgbw"] : []),
|
||||
...(infoData.leds?.lc & 0x04 ? ["cct"] : []),
|
||||
...((infoData.leds?.maxpwr ?? 0) > 0 ? ["abl"] : []),
|
||||
...(cfgData.hw?.led?.cr ? ["cct-from-rgb"] : []),
|
||||
...(cfgData.hw?.led?.cct ? ["white-balance"] : []),
|
||||
...((cfgData.light?.gc?.col ?? 1.0) > 1.0 || (cfgData.light?.gc?.bri ?? 1.0) > 1.0 ? ["gamma"] : []),
|
||||
...(cfgData.light?.aseg ? ["auto-segments"] : []),
|
||||
...((cfgData.light?.nl?.mode ?? 0) > 0 ? ["nightlight"] : []),
|
||||
],
|
||||
|
||||
// peripherals (note: i2c/spi may reflect board defaults, not user-configured hardware)
|
||||
peripherals: [
|
||||
...((cfgData.hw?.relay?.pin ?? -1) >= 0 ? ["relay"] : []),
|
||||
...((cfgData.hw?.btn?.ins ?? []).filter(b => b.type !== 0).length > 0 ? ["buttons"] : []),
|
||||
...((cfgData.eth?.type ?? 0) > 0 ? ["ethernet"] : []),
|
||||
...((cfgData.if?.live?.dmx?.inputRxPin ?? 0) > 0 ? ["dmx-input"] : []),
|
||||
...((cfgData.hw?.ir?.type ?? 0) > 0 ? ["ir-remote"] : []),
|
||||
],
|
||||
buttonCount: (cfgData.hw?.btn?.ins ?? []).filter(b => b.type !== 0).length,
|
||||
|
||||
// integrations
|
||||
integrations: [
|
||||
...(cfgData.if?.hue?.en ? ["hue"] : []),
|
||||
...(cfgData.if?.mqtt?.en ? ["mqtt"] : []),
|
||||
...(cfgData.if?.va?.alexa ? ["alexa"] : []),
|
||||
...(cfgData.if?.sync?.send?.en ? ["wled-sync"] : []),
|
||||
...(cfgData.nw?.espnow ? ["esp-now"] : []),
|
||||
...(cfgData.if?.sync?.espnow ? ["esp-now-sync"] : []),
|
||||
],
|
||||
|
||||
// usermods
|
||||
usermods: Object.keys(cfgData.um ?? {}),
|
||||
usermodIds: infoData.um ?? [],
|
||||
};
|
||||
|
||||
// IR remote — only include if configured
|
||||
const irType = IR_TYPES[cfgData.hw?.ir?.type ?? 0];
|
||||
if (irType) upgradeData.irRemoteType = irType;
|
||||
repo: infoData.repo // GitHub repository (always present)
|
||||
};
|
||||
|
||||
// Add optional fields if available
|
||||
if (infoData.psrSz !== undefined) upgradeData.psramSize = infoData.psrSz; // Total PSRAM size in MB; can be 0
|
||||
|
||||
// Note: partitionSizes not currently available in /json/info endpoint
|
||||
|
||||
// Make AJAX call to postUpgradeEvent API
|
||||
return fetch('https://usage.wled.me/api/usage/upgrade', {
|
||||
@@ -3550,17 +3471,6 @@ function reportUpgradeEvent(info, oldVersion, alwaysReport) {
|
||||
});
|
||||
}
|
||||
|
||||
function busTypeToString(t) {
|
||||
if (t === 0) return "none";
|
||||
if (t === 40) return "on-off";
|
||||
if (t >= 16 && t <= 39) return "digital"; // WS2812, SK6812, etc.
|
||||
if (t >= 41 && t <= 47) return "pwm"; // analog RGB/CCT/single
|
||||
if (t >= 48 && t <= 63) return "digital-spi"; // APA102, WS2801, etc.
|
||||
if (t >= 64 && t <= 71) return "hub75"; // HUB75 matrix panels
|
||||
if (t >= 80 && t <= 95) return "network"; // DDP, E1.31, ArtNet
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
function updateVersionInfo(version, neverAsk, alwaysReport) {
|
||||
const versionInfo = {
|
||||
version: version,
|
||||
|
||||
@@ -424,7 +424,7 @@ let fL; // file list
|
||||
const classics=['console_font_4x6.wbf','console_font_5x12.wbf','console_font_5x8.wbf','console_font_6x8.wbf','console_font_7x9.wbf']; // classic WLED fonts list
|
||||
let pT = []; // local tools list from JSON
|
||||
let wv = [0, 0]; // wled version [major, minor], updated in fsMem(), used to check tool compatibility
|
||||
const remoteURL = 'https://wled.github.io/wled-web-tools/pftools.json'; // tools list
|
||||
const remoteURL = 'https://dedehai.github.io/pftools.json'; // Change to your actual repo
|
||||
const toolsjson = 'pftools.json';
|
||||
// note: the pftools.json must use major.minor for tool versions (e.g. 0.95 or 1.1), otherwise the update check won't work
|
||||
// also the code assumes that the tool url points to a gz file
|
||||
@@ -498,7 +498,7 @@ function segLoad(){
|
||||
}).catch(console.error);
|
||||
}
|
||||
|
||||
/* which seg is showing image fx (ID 53) */
|
||||
/* which seg is showing image fx 53 */
|
||||
function curImgSeg(){
|
||||
const sel=getId('seg');
|
||||
for(let i=0;i<sel.options.length;i++){
|
||||
|
||||
@@ -9,21 +9,20 @@
|
||||
(function loadFiles() {
|
||||
const l = document.createElement('script');
|
||||
l.src = 'common.js';
|
||||
// note: loading style.css here causes ugly white button flashes so @import url("style.css") below inlines it for this page (only costs ~30bytes of flash)
|
||||
l.onload = () => {
|
||||
// load style.css then initialize
|
||||
l.onload = () => loadResources(['style.css'], () => {
|
||||
getLoc();
|
||||
loadJS(getURL('/settings/s.js?p=0'), false);
|
||||
};
|
||||
});
|
||||
l.onerror = () => setTimeout(loadFiles, 100);
|
||||
document.head.appendChild(l);
|
||||
})();
|
||||
</script>
|
||||
<style>
|
||||
@import url("style.css");
|
||||
@import url("style.css");
|
||||
body {
|
||||
height: 100px;
|
||||
margin: 0 auto;
|
||||
max-width: 520px;
|
||||
margin: 0;
|
||||
}
|
||||
html {
|
||||
--h: 9vh;
|
||||
@@ -31,7 +30,7 @@
|
||||
button {
|
||||
display: block;
|
||||
border-radius: var(--h);
|
||||
font-size: clamp(1rem, 6vw, 2rem);
|
||||
font-size: 6vmin;
|
||||
height: var(--h);
|
||||
width: calc(100% - 40px);
|
||||
margin: 2vh auto 0;
|
||||
|
||||
+112
-102
@@ -7,7 +7,6 @@
|
||||
<style> html { visibility: hidden; } </style> <!-- prevent white & ugly display while loading, unhidden in loadResources() -->
|
||||
<script>
|
||||
var maxD=1,maxI2S=0,maxRMT=0,maxA=1,chipID=0,maxM=4000,maxPB=2048,maxL=1664,maxCO=5,maxBT=4; //maximum bytes for LED allocation: 4kB for 8266, 32kB for 32
|
||||
var hasPSRAM=false; // set from /json/info on page load
|
||||
var customStarts=false,startsDirty=[];
|
||||
function off(n) { gN(n).value = -1;}
|
||||
// these functions correspond to C macros found in const.h
|
||||
@@ -39,10 +38,6 @@
|
||||
function S() {
|
||||
getLoc();
|
||||
if(localStorage.getItem('ASc')==='true') d.Sf.AS.checked=true;
|
||||
fetch(getURL('/json/info'))
|
||||
.then(r => r.ok ? r.json() : {})
|
||||
.then(info => { hasPSRAM = Number(info.psrSz ?? 0) > 0 || Number(info.psram ?? 0) > 0; })
|
||||
.catch(() => {});
|
||||
loadJS(getURL('/settings/s.js?p=2'), false, ()=>{
|
||||
d.ledTypes = [/*{i:22,c:1,t:"D",n:"WS2812"},{i:42,c:6,t:"AA",n:"PWM CCT"}*/]; // filled from GetV()
|
||||
d.um_p = [];
|
||||
@@ -54,7 +49,7 @@
|
||||
setABL();
|
||||
d.Sf.addEventListener("submit", trySubmit);
|
||||
if (d.um_p[0]==-1) d.um_p.shift();
|
||||
fetchPinInfo(pinDropdowns);
|
||||
pinDropdowns();
|
||||
}); // If we set async false, file is loaded and executed, then next statement is processed
|
||||
if (loc) d.Sf.action = getURL('/settings/leds');
|
||||
}
|
||||
@@ -75,10 +70,9 @@
|
||||
function isS2() { return chipID == 2; }
|
||||
function isS3() { return chipID == 3; }
|
||||
function is32() { return chipID == 4; }
|
||||
|
||||
function pinsOK() {
|
||||
var ok = true;
|
||||
var nList = Array.from(d.Sf.querySelectorAll("#mLC input[name^=L], #mLC select.pin[name^=L]")); // include both input types (numeric & dropdown)
|
||||
var nList = d.Sf.querySelectorAll("#mLC input[name^=L]");
|
||||
nList.forEach((LC,i)=>{
|
||||
if (!ok) return; // prevent iteration after conflict
|
||||
let nm = LC.name.substring(0,2); // field name : /L./
|
||||
@@ -90,28 +84,19 @@
|
||||
}
|
||||
// ignore IP address
|
||||
if (isNet(t)) return;
|
||||
// LED pin(s) must be assigned for non-virtual types
|
||||
let pIdx = parseInt(LC.name.charAt(1)); // determine pin index (0 for L0, 1 for L1, etc.)
|
||||
if (pIdx < numPins(t) && !isVir(t) && (LC.value === "" || LC.value === "-1")) {
|
||||
LC.focus();
|
||||
ok = false;
|
||||
return;
|
||||
}
|
||||
//check for pin conflicts
|
||||
if (LC.value!="" && LC.value!="-1") {
|
||||
let p = d.rsvd.concat(d.um_p); // used pin array
|
||||
//d.Sf.querySelectorAll("select.pin").forEach((e)=>{if(e.value>-1)p.push(parseInt(e.value));}) // buttons, IR & relay
|
||||
// only non-LED selects (buttons, IR, relay) — LED pins check against each other below
|
||||
d.Sf.querySelectorAll("select.pin").forEach((e)=>{if(!/^L[0-4]/.test(e.name) && e.value>-1)p.push(parseInt(e.value));})
|
||||
d.Sf.querySelectorAll("select.pin").forEach((e)=>{if(e.value>-1)p.push(parseInt(e.value));}) // buttons, IR & relay
|
||||
if (p.some((e)=>e==parseInt(LC.value))) {
|
||||
alert(`Sorry, pins ${JSON.stringify(p)} can't be used.`);
|
||||
if (LC.tagName==="SELECT") LC.value="-1"; else LC.value="";
|
||||
LC.value="";
|
||||
LC.focus();
|
||||
ok = false;
|
||||
return;
|
||||
} else if (d.ro_gpio.some((e)=>e==parseInt(LC.value))) {
|
||||
alert(`Sorry, pins ${JSON.stringify(d.ro_gpio)} are input only.`);
|
||||
if (LC.tagName==="SELECT") LC.value="-1"; else LC.value="";
|
||||
LC.value="";
|
||||
LC.focus();
|
||||
ok = false;
|
||||
return;
|
||||
@@ -122,9 +107,10 @@
|
||||
let m = nList[j].name.substring(2,3); // bus number (0-Z)
|
||||
let t2 = parseInt(gN("LT"+m).value, 10);
|
||||
if (isVir(t2)) continue;
|
||||
if (nList[j].value!="" && nList[j].value!="-1" && LC.value==nList[j].value) {
|
||||
if (nList[j].value!="" && nList[i].value==nList[j].value) {
|
||||
alert(`Pin conflict between ${LC.name}/${nList[j].name}!`);
|
||||
if (nList[j].tagName==="SELECT") nList[j].value="-1"; else { nList[j].value=""; nList[j].focus(); }
|
||||
nList[j].value="";
|
||||
nList[j].focus();
|
||||
ok = false;
|
||||
return;
|
||||
}
|
||||
@@ -152,7 +138,7 @@
|
||||
if (isHub75(t)) {
|
||||
let p = parseInt(d.Sf["L2"+n].value)||1, r = parseInt(d.Sf["L3"+n].value)||1, c = parseInt(d.Sf["L4"+n].value)||1, h = parseInt(d.Sf["L1"+n].value)||1;
|
||||
if (r*c !== p) {alert(`HUB75 error: panels≠rows×cols`); e.stopPropagation(); return false;}
|
||||
if (h >= 64 && p > 1 && !hasPSRAM) {alert(`HUB75 error: height >= 64, only single panel allowed`); e.stopPropagation(); return false;}
|
||||
if (h >= 64 && p > 1) {alert(`HUB75 error: height >= 64, only single panel allowed`); e.stopPropagation(); return false;}
|
||||
if(isS3()) {
|
||||
alert("HUB75 changes require a reboot"); // TODO: only throw this if panel config changed?
|
||||
}
|
||||
@@ -317,28 +303,14 @@
|
||||
gId("p0d"+n).innerText = p0d;
|
||||
gId("p1d"+n).innerText = p1d;
|
||||
gId("off"+n).innerText = off;
|
||||
// convert pin fields so show/hide applies to the correct element type
|
||||
for (let p=0; p<5; p++) {
|
||||
let nm2 = "L"+p+n;
|
||||
let el = d.Sf[nm2];
|
||||
if (!el) continue;
|
||||
if (isVir(t) || isHub75(t)) {
|
||||
if (el.tagName === "SELECT") unmakePinSelect(nm2); // see common.js
|
||||
} else {
|
||||
if (el.tagName === "INPUT" && el.type === "number") {
|
||||
makePinSelect(nm2, 1); // see common.js
|
||||
d.pinUpdPending = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// show/hide secondary pins on whatever element type now exists
|
||||
let pins = Math.max(gT(t).t.length,1) + 3*isNet(t) + 4*isHub75(t);
|
||||
// secondary pins show/hide (type string length is equivalent to number of pins used; except for network and on/off)
|
||||
let pins = Math.max(gT(t).t.length,1) + 3*isNet(t) + 4*isHub75(t); // fixes network pins to 4
|
||||
for (let p=1; p<5; p++) {
|
||||
var LK = d.Sf["L"+p+n];
|
||||
if (!LK) continue;
|
||||
LK.style.display = (p < pins) ? "inline" : "none";
|
||||
LK.required = (p < pins);
|
||||
if (p >= pins) LK.value = (LK.tagName === "SELECT") ? "-1" : "";
|
||||
if (p >= pins) LK.value="";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -443,6 +415,25 @@
|
||||
LC.style.color="#fff";
|
||||
return; // do not check conflicts
|
||||
}
|
||||
// check for pin conflicts & color fields
|
||||
if (nm.search(/^L[0-4]/) == 0) // pin fields
|
||||
if (LC.value!="" && LC.value!="-1") {
|
||||
let p = d.rsvd.concat(d.um_p); // used pin array
|
||||
d.Sf.querySelectorAll("select.pin").forEach((e)=>{if(e.value>-1)p.push(parseInt(e.value));}) // buttons, IR & relay
|
||||
for (j=0; j<nList.length; j++) {
|
||||
if (i==j) continue;
|
||||
let n2 = nList[j].name.substring(0,2); // field name : /L./
|
||||
if (n2.search(/^L[0-4]/) == 0) { // pin fields
|
||||
let m = nList[j].name.substring(2,3); // bus number (0-Z)
|
||||
let t2 = parseInt(gN("LT"+m).value, 10);
|
||||
if (isVir(t2)) continue;
|
||||
if (nList[j].value!="" && nList[j].value!="-1") p.push(parseInt(nList[j].value,10)); // add current pin
|
||||
}
|
||||
}
|
||||
// now check for conflicts
|
||||
if (p.some((e)=>e==parseInt(LC.value))) LC.style.color = "red";
|
||||
else LC.style.color = d.ro_gpio.some((e)=>e==parseInt(LC.value)) ? "orange" : "#fff";
|
||||
} else LC.style.color = "#fff";
|
||||
});
|
||||
|
||||
// Use helper function to calculate channel usage
|
||||
@@ -519,11 +510,6 @@
|
||||
gId('fpsNone').style.display = (d.Sf.FR.value == 0) ? 'block':'none';
|
||||
gId('fpsWarn').style.display = (d.Sf.FR.value == 0) || (d.Sf.FR.value >= 80) ? 'block':'none';
|
||||
gId('fpsHigh').style.display = (d.Sf.FR.value >= 80) ? 'block':'none';
|
||||
|
||||
if (d.pinUpdPending) {
|
||||
d.pinUpdPending = false;
|
||||
d.Sf.querySelectorAll("select.pin").forEach((e) => { pinUpd(e); });
|
||||
}
|
||||
}
|
||||
|
||||
function lastEnd(i) {
|
||||
@@ -685,18 +671,11 @@ Swap: <select id="xw${s}" name="XW${s}">
|
||||
gId("com_rem").style.display = (i>0) ? "inline":"none";
|
||||
}
|
||||
|
||||
// get pin dropdown flags for button type: touch=2, ADC=4, any=0
|
||||
function btnPinFlags(t) { return (t==6||t==9) ? 2 : (t==7||t==8) ? 4 : 0; }
|
||||
function btnPinDd(s) {
|
||||
let t = parseInt(d.Sf["BE"+s].value);
|
||||
makePinSelect("BT"+s, btnPinFlags(t));
|
||||
d.Sf.querySelectorAll("select.pin").forEach(e => pinUpd(e));
|
||||
}
|
||||
function addBtn(i,p,t) {
|
||||
var b = gId("btns");
|
||||
var s = chrID(i);
|
||||
var c = `<div id="btn${i}">#${i} GPIO: <input type="number" name="BT${s}" onchange="UI()" min="-1" max="${d.max_gpio}" class="xs" value="${p}">`;
|
||||
c += ` <select name="BE${s}" onchange="btnPinDd('${s}')">`
|
||||
c += ` <select name="BE${s}">`
|
||||
c += `<option value="0" ${t==0?"selected":""}>Disabled</option>`;
|
||||
c += `<option value="2" ${t==2?"selected":""}>Pushbutton</option>`;
|
||||
c += `<option value="3" ${t==3?"selected":""}>Push inverted</option>`;
|
||||
@@ -826,70 +805,101 @@ Swap: <select id="xw${s}" name="XW${s}">
|
||||
}
|
||||
}
|
||||
function pinDropdowns() {
|
||||
// Rebuild LED GPIO selects now that d.pinsData is available.
|
||||
d.Sf.querySelectorAll(".iST input.s[name^=L], .iST select.pin[name^=L]").forEach((e) => {
|
||||
let n = e.name.substring(2, 3);
|
||||
let t = parseInt(d.Sf["LT"+n].value, 10);
|
||||
if (!isVir(t) && !isHub75(t)) makePinSelect(e.name, 1);
|
||||
});
|
||||
// IR (any pin including input-only)
|
||||
let irSel = makePinSelect("IR", 0);
|
||||
if (irSel) irSel.onchange = function() { UI(); pinUpd(this); };
|
||||
// Relay (output required)
|
||||
let rlSel = makePinSelect("RL", 1);
|
||||
if (rlSel) rlSel.onchange = function() { UI(); pinUpd(this); };
|
||||
// Buttons (flags depend on button type: touch=2, ADC=4)
|
||||
gId("btns").querySelectorAll('input[type="number"], select.pin[name^=BT]').forEach((e) => {
|
||||
let s = e.name.substring(2);
|
||||
let t = parseInt(d.Sf["BE"+s]?.value) || 0;
|
||||
let bSel = makePinSelect(e.name, btnPinFlags(t));
|
||||
if (bSel) bSel.onchange = function() { UI(); pinUpd(this); };
|
||||
});
|
||||
// cross-update all pin selects
|
||||
d.Sf.querySelectorAll("select.pin").forEach((e) => { pinUpd(e); });
|
||||
// add dataset values for remaining LED GPIO inputs (virtual/HUB75)
|
||||
d.Sf.querySelectorAll(".iST input.s[name^=L]").forEach((i) => {
|
||||
if (i.value !== "" && parseInt(i.value, 10) >= 0)
|
||||
let fields = ["IR","RL"]; // IR & relay
|
||||
gId("btns").querySelectorAll('input[type="number"]').forEach((e)=>{fields.push(e.name);}) // buttons
|
||||
for (let i of d.Sf.elements) {
|
||||
if (i.type === "number" && fields.includes(i.name)) { //select all pin select elements
|
||||
let v = parseInt(i.value);
|
||||
let sel = addDropdown(i.name,0);
|
||||
for (var j = -1; j < d.max_gpio; j++) {
|
||||
if (d.rsvd.includes(j)) continue;
|
||||
let foundPin = d.um_p.indexOf(j);
|
||||
let txt = (j === -1) ? "unused" : `${j}`;
|
||||
if (foundPin >= 0 && j !== v) txt += ` used`; // already reserved pin
|
||||
if (d.ro_gpio.includes(j)) txt += " (R/O)";
|
||||
let opt = addOption(sel, txt, j);
|
||||
if (j === v) opt.selected = true; // this is "our" pin
|
||||
else if (d.um_p.includes(j) && j > -1) opt.disabled = true; // someone else's pin
|
||||
}
|
||||
}
|
||||
}
|
||||
// update select options
|
||||
d.Sf.querySelectorAll("select.pin").forEach((e)=>{pinUpd(e);});
|
||||
// add dataset values for LED GPIO pins
|
||||
d.Sf.querySelectorAll(".iST input.s[name^=L]").forEach((i)=>{
|
||||
if (i.value!=="" && i.value>=0)
|
||||
i.dataset.val = i.value;
|
||||
});
|
||||
}
|
||||
function pinUpd(e) {
|
||||
// update changed select options across all pin selects
|
||||
// update changed select options across all usermods
|
||||
let oldV = parseInt(e.dataset.val);
|
||||
e.dataset.val = e.value;
|
||||
let label = /^L[0-4].$/.test(e.name) ? 'LED' : e.name;
|
||||
let txt = e.name;
|
||||
let pins = [];
|
||||
// collect LED bus pin values from remaining inputs (virtual/HUB75)
|
||||
d.Sf.querySelectorAll(".iST input.s[name^=L]").forEach((i) => {
|
||||
let busN = i.name.substring(2, 3);
|
||||
let t = parseInt(d.Sf["LT"+busN].value, 10);
|
||||
let p = parseInt(i.name.charAt(1), 10);
|
||||
if (isVir(t) || isHub75(t) || p >= numPins(t)) return;
|
||||
if (i.value !== "" && parseInt(i.value, 10) >= 0) pins.push(i.value);
|
||||
});
|
||||
// collect LED bus pin values from selects
|
||||
d.Sf.querySelectorAll(".iST select.pin[name^=L]").forEach((s) => {
|
||||
if (s !== e && parseInt(s.value) >= 0)
|
||||
pins.push(s.value);
|
||||
d.Sf.querySelectorAll(".iST input.s[name^=L]").forEach((i)=>{
|
||||
if (i.value!=="" && i.value>=0 && i.max<255)
|
||||
pins.push(i.value);
|
||||
});
|
||||
let selects = d.Sf.querySelectorAll("select.pin");
|
||||
for (let sel of selects) {
|
||||
if (sel == e) continue;
|
||||
Array.from(sel.options).forEach((i) => {
|
||||
if (i.value == sel.dataset.val) return; // skip sel's own selected pin
|
||||
if (sel == e) continue
|
||||
Array.from(sel.options).forEach((i)=>{
|
||||
let led = pins.includes(i.value);
|
||||
if (!(i.value == oldV || i.value == e.value || led)) return;
|
||||
if (i.value == -1) { i.text = "unused"; return; }
|
||||
i.text = i.value;
|
||||
if (i.value == oldV) { i.disabled = false; }
|
||||
if (i.value == e.value || led) {
|
||||
i.disabled = true;
|
||||
i.text += ` ${led ? 'LED' : label}`;
|
||||
if (!(i.value==oldV || i.value==e.value || led)) return;
|
||||
if (i.value == -1) {
|
||||
i.text = "unused";
|
||||
return
|
||||
}
|
||||
//if (d.ro_gpio.includes(parseInt(i.value))) i.text += " (R/O)"; // read only pin note: removed as pin is not shown for outputs
|
||||
i.text = i.value;
|
||||
if (i.value==oldV) {
|
||||
i.disabled = false;
|
||||
}
|
||||
if (i.value==e.value || led) {
|
||||
i.disabled = true;
|
||||
i.text += ` ${led?'LED':txt}`;
|
||||
}
|
||||
if (d.ro_gpio.includes(parseInt(i.value))) i.text += " (R/O)";
|
||||
});
|
||||
}
|
||||
}
|
||||
// https://stackoverflow.com/questions/39729741/javascript-change-input-text-to-select-option
|
||||
function addDropdown(field) {
|
||||
let sel = cE('select');
|
||||
sel.classList.add("pin");
|
||||
let inp = d.getElementsByName(field)[0];
|
||||
if (inp && inp.tagName === "INPUT" && (inp.type === "text" || inp.type === "number")) { // may also use nodeName
|
||||
let v = inp.value;
|
||||
let n = inp.name;
|
||||
// copy the existing input element's attributes to the new select element
|
||||
for (var i = 0; i < inp.attributes.length; ++ i) {
|
||||
var att = inp.attributes[i];
|
||||
// type and value don't apply, so skip them
|
||||
// ** you might also want to skip style, or others -- modify as needed **
|
||||
if (att.name != 'type' && att.name != 'value' && att.name != 'class' && att.name != 'style') {
|
||||
sel.setAttribute(att.name, att.value);
|
||||
}
|
||||
}
|
||||
sel.setAttribute("data-val", v);
|
||||
sel.setAttribute("onchange", "pinUpd(this)");
|
||||
// finally, replace the old input element with the new select element
|
||||
inp.parentElement.replaceChild(sel, inp);
|
||||
return sel;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function addOption(sel,txt,val) {
|
||||
if (sel===null) return; // select object missing
|
||||
let opt = cE("option");
|
||||
opt.value = val;
|
||||
opt.text = txt;
|
||||
sel.appendChild(opt);
|
||||
for (let i=0; i<sel.childNodes.length; i++) {
|
||||
let c = sel.childNodes[i];
|
||||
if (c.value == sel.dataset.val) sel.selectedIndex = i;
|
||||
}
|
||||
return opt;
|
||||
}
|
||||
// calculate channel usage across all buses
|
||||
function getDuse() {
|
||||
let rmtUsed = 0, i2sUsed = 0;
|
||||
|
||||
@@ -16,11 +16,44 @@
|
||||
var pinsTimer=null, gpioInfo={};
|
||||
function S() {
|
||||
getLoc();
|
||||
fetchPinInfo(()=>{ loadPins(); pinsTimer=setInterval(loadPins,250); });
|
||||
loadJS(getURL('/settings/s.js?p=11'), false, ()=>{
|
||||
d.um_p = [];
|
||||
d.rsvd = [];
|
||||
d.ro_gpio = [];
|
||||
d.max_gpio = 50;
|
||||
d.touch = [];
|
||||
}, ()=>{
|
||||
// Load extended GPIO info and start pin polling
|
||||
loadPins();
|
||||
pinsTimer = setInterval(loadPins, 250);
|
||||
});
|
||||
}
|
||||
|
||||
function B(){window.open(getURL('/settings'),'_self');} // back button
|
||||
|
||||
function getOwnerName(o,t,n) {
|
||||
// Use firmware-provided name if available
|
||||
if(n) return n;
|
||||
if(!o) return "System"; // no owner provided
|
||||
if(o===0x85){ return getBtnTypeName(t); } // button pin
|
||||
return "UM #"+o;
|
||||
}
|
||||
function getBtnTypeName(t) {
|
||||
var n=["None","Reserved","Push","Push Inv","Switch","PIR","Touch","Analog","Analog Inv","Touch Switch"];
|
||||
var label = n[t] || "?";
|
||||
return 'Button <span style="font-size:10px;color:#888">'+label+'</span>';
|
||||
}
|
||||
function getCaps(p,c) {
|
||||
var r=[];
|
||||
// Use touch info from settings endpoint
|
||||
if(d.touch && d.touch.includes(p)) r.push("Touch");
|
||||
if(d.ro_gpio && d.ro_gpio.includes(p)) r.push("Input Only");
|
||||
// Use other caps from JSON (Analog, Boot, Input Only)
|
||||
if(c&0x02) r.push("Analog");
|
||||
if(c&0x08) r.push("Flash Boot");
|
||||
if(c&0x10) r.push("Bootstrap");
|
||||
return r.length?r.join(", "):"-";
|
||||
}
|
||||
function loadPins() {
|
||||
fetch(getURL('/json/pins'),{method:'get'})
|
||||
.then(r=>r.json())
|
||||
|
||||
@@ -42,30 +42,12 @@
|
||||
function SetVal(){switch(parseInt(d.Sf.EP.value)){case 5568: d.Sf.DI.value = 5568; break; case 6454: d.Sf.DI.value = 6454; break; case 4048: d.Sf.DI.value = 4048; break; }; SP();FC();}
|
||||
function S(){
|
||||
getLoc();
|
||||
loadJS(getURL('/settings/s.js?p=4'), false, undefined, ()=>{SetVal(); fetchPinInfo(pinDropdowns);}); // If we set async false, file is loaded and executed, then next statement is processed
|
||||
loadJS(getURL('/settings/s.js?p=4'), false, undefined, ()=>{SetVal();}); // If we set async false, file is loaded and executed, then next statement is processed
|
||||
if (loc) d.Sf.action = getURL('/settings/sync');
|
||||
}
|
||||
function getURL(path) {
|
||||
return (loc ? locproto + "//" + locip : "") + path;
|
||||
}
|
||||
const dmxNs = ["IDMR","IDMT","IDME"];
|
||||
let dmxOwnPins; // track dmx pins to update dropdown restrictions on pin change
|
||||
function pinDropdowns() {
|
||||
dmxNs.forEach((n,i) => { const s = makePinSelect(n, i?1:0); if (s) s.onchange = dmxPinUpd; }); // note on the i?1:0 -> RX pin is allowed input only but TX/EN pins must be GPIO
|
||||
dmxOwnPins = dmxNs.map(n => parseInt(gN(n)?.value??-1));
|
||||
dmxPinUpd();
|
||||
}
|
||||
function dmxPinUpd() {
|
||||
const vs = dmxNs.map(n => parseInt(gN(n)?.value??-1));
|
||||
dmxNs.forEach((n,i) => {
|
||||
for (const o of gN(n)?.options??[]) {
|
||||
const p = parseInt(o.value); if (p<0) continue;
|
||||
// unlock old selected pin, lock newly selected one
|
||||
o.disabled = (p!==vs[i] && !dmxOwnPins.includes(p) && !!d.pinsData?.find(pi=>pi.p===p)?.a) || vs.some((v,j)=>j!==i&&v===p);
|
||||
if (!o.disabled) o.text = String(p); // clear owner name i.e. (DMX input)
|
||||
}
|
||||
});
|
||||
}
|
||||
function hideDMXInput(){gId("dmxInput").style.display="none";}
|
||||
function hideNoDMXInput(){gId("dmxInputOff").style.display="none";}
|
||||
function hideNoDMXOutput(){gId("dmxOnOffOutput").style.display="none";}
|
||||
@@ -144,8 +126,8 @@ Send notifications on direct change: <input type="checkbox" name="SD"><br>
|
||||
Send notifications on button press or IR: <input type="checkbox" name="SB"><br>
|
||||
Send Alexa notifications: <input type="checkbox" name="SA"><br>
|
||||
Send Philips Hue change notifications: <input type="checkbox" name="SH"><br>
|
||||
UDP packet retransmissions: <input name="UR" type="number" min="0" max="30" class="d5" required><br>
|
||||
<i class="warn">Reboot required to apply changes. </i>
|
||||
UDP packet retransmissions: <input name="UR" type="number" min="0" max="30" class="d5" required><br><br>
|
||||
<i>Reboot required to apply changes. </i>
|
||||
</div>
|
||||
<div class="sec">
|
||||
<h3>Instance List</h3>
|
||||
@@ -157,7 +139,7 @@ Make this instance discoverable: <input type="checkbox" name="NB">
|
||||
Receive UDP realtime: <input type="checkbox" name="RD"><br>
|
||||
Use main segment only: <input type="checkbox" name="MO"><br>
|
||||
Respect LED Maps: <input type="checkbox" name="RLM"><br><br>
|
||||
<h4>Network DMX input</h4><br>
|
||||
<i>Network DMX input</i><br>
|
||||
Type:
|
||||
<select name=DI onchange="SP(); adj();">
|
||||
<option value=5568>E1.31 (sACN)</option>
|
||||
@@ -192,14 +174,12 @@ Force max brightness: <input type="checkbox" name="FB"><br>
|
||||
Disable realtime gamma correction: <input type="checkbox" name="RG"><br>
|
||||
Realtime LED offset: <input name="WO" type="number" min="-255" max="255" required>
|
||||
<div id="dmxInput">
|
||||
<br>
|
||||
<h4>Wired DMX Input</h4>
|
||||
DMX RX Pin: <input name="IDMR" type="number" min="-1" max="99">RO<br/>
|
||||
DMX TX Pin: <input name="IDMT" type="number" min="-1" max="99">DI<br/>
|
||||
DMX Enable Pin: <input name="IDME" type="number" min="-1" max="99">RE+DE<br/>
|
||||
<h4>Wired DMX Input Pins</h4>
|
||||
DMX RX: <input name="IDMR" type="number" min="-1" max="99">RO<br/>
|
||||
DMX TX: <input name="IDMT" type="number" min="-1" max="99">DI<br/>
|
||||
DMX Enable: <input name="IDME" type="number" min="-1" max="99">RE+DE<br/>
|
||||
DMX Port: <input name="IDMP" type="number" min="1" max="2"><br/>
|
||||
<i class="warn">Reboot required to apply changes.</i>
|
||||
</div>
|
||||
</div>
|
||||
<div id="dmxInputOff">
|
||||
<br><i class="warn">This firmware build does not include DMX Input support. <br></i>
|
||||
</div>
|
||||
@@ -243,7 +223,7 @@ Device Topic: <input type="text" name="MD" maxlength="32"><br>
|
||||
Group Topic: <input type="text" name="MG" maxlength="32"><br>
|
||||
Publish on button press: <input type="checkbox" name="BM"><br>
|
||||
Retain brightness & color messages: <input type="checkbox" name="RT"><br>
|
||||
<i class="warn">Reboot required to apply changes. </i><a href="https://kno.wled.ge/interfaces/mqtt/" target="_blank">MQTT info</a>
|
||||
<i>Reboot required to apply changes. </i><a href="https://kno.wled.ge/interfaces/mqtt/" target="_blank">MQTT info</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sec">
|
||||
|
||||
+33
-10
@@ -7,6 +7,7 @@
|
||||
<style> html { visibility: hidden; } </style> <!-- prevent white & ugly display while loading, unhidden in loadResources() -->
|
||||
<script>
|
||||
var umCfg = {};
|
||||
var pins = [], pinO = [], owner;
|
||||
var urows;
|
||||
var numM = 0;
|
||||
// load common.js with retry on error
|
||||
@@ -31,6 +32,7 @@
|
||||
})
|
||||
.then(json => {
|
||||
umCfg = json.um;
|
||||
getPins(json);
|
||||
urows="";
|
||||
if (isO(umCfg)) {
|
||||
for (const [k,o] of Object.entries(umCfg)) {
|
||||
@@ -48,9 +50,11 @@
|
||||
d.ro_gpio = [];
|
||||
d.extra = [];
|
||||
}, ()=>{
|
||||
for (let r of d.rsvd) { pins.push(r); pinO.push("rsvd"); } // reserved pins
|
||||
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;
|
||||
fetchPinInfo(pinDD); // load pin occupancy, then convert INPUT to SELECT for pins
|
||||
//for (let i of d.getElementsByTagName("input")) if (i.type === "number" && i.name.replace("[]","").substr(-3) === "pin") i.max = d.max_gpio;
|
||||
pinDD(); // convert INPUT to SELECT for pins
|
||||
}); // If we set async false, file is loaded and executed, then next statement is processed
|
||||
})
|
||||
.catch((error)=>{
|
||||
@@ -86,7 +90,28 @@
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
function getPins(o) {
|
||||
if (isO(o)) {
|
||||
for (const [k,v] of Object.entries(o)) {
|
||||
if (isO(v)) {
|
||||
let oldO = owner; // keep parent name
|
||||
owner = k;
|
||||
getPins(v);
|
||||
owner = oldO;
|
||||
continue;
|
||||
}
|
||||
if (k.replace("[]","").substr(-3)=="pin") {
|
||||
if (Array.isArray(v)) {
|
||||
for (var i=0; i<v.length; i++) if (v[i]>=0) { pins.push(v[i]); pinO.push(owner); }
|
||||
} else {
|
||||
if (v>=0) { pins.push(v); pinO.push(owner); }
|
||||
}
|
||||
} else if (Array.isArray(v)) {
|
||||
for (var i=0; i<v.length; i++) getPins(v[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
function initCap(s) {
|
||||
if (typeof s !== 'string') return '';
|
||||
// https://www.freecodecamp.org/news/how-to-capitalize-words-in-javascript/
|
||||
@@ -136,17 +161,15 @@
|
||||
if (i.type === "number" && (i.name.includes("pin") || ["SDA","SCL","MOSI","MISO","SCLK"].includes(i.name))) { //select all pin select elements
|
||||
let v = parseInt(i.value);
|
||||
let sel = addDD(i.name,0);
|
||||
if (!sel) continue;
|
||||
for (var j = -1; j < d.max_gpio; j++) {
|
||||
if (j > -1 && d.rsvd && d.rsvd.includes(j)) continue;
|
||||
let pInfo = d.pinsData && d.pinsData.find(p => p.p === j);
|
||||
let used = j > -1 && pInfo && pInfo.a && j !== v; // add owner info if used and not "our" pin
|
||||
if (d.rsvd.includes(j)) continue;
|
||||
let foundPin = pins.indexOf(j);
|
||||
let txt = (j === -1) ? "unused" : `${j}`;
|
||||
if (used) txt += ` (${getOwnerName(pInfo.o, pInfo.t, pInfo.n)})`;
|
||||
if (j > -1 && d.ro_gpio && d.ro_gpio.includes(j)) txt += " (input only)"; // read-only pin
|
||||
if (foundPin >= 0 && j !== v) txt += ` ${pinO[foundPin]=="if"?"global":pinO[foundPin]}`; // already reserved pin
|
||||
if (d.ro_gpio.includes(j)) txt += " (R/O)";
|
||||
let opt = addO(sel, txt, j);
|
||||
if (j === v) opt.selected = true; // this is "our" pin
|
||||
else if (used) opt.disabled = true; // someone else's pin
|
||||
else if (pins.includes(j)) opt.disabled = true; // someone else's pin
|
||||
}
|
||||
let um = i.name.split(":")[0];
|
||||
d.extra.forEach((o)=>{
|
||||
@@ -181,7 +204,7 @@
|
||||
i.disabled = true;
|
||||
i.text += ` ${txt}`;
|
||||
}
|
||||
if (d.ro_gpio.includes(parseInt(i.value))) i.text += " (input only)"; // read-only pin
|
||||
if (d.ro_gpio.includes(parseInt(i.value))) i.text += " (R/O)";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -236,10 +236,8 @@ Static subnet mask:<br>
|
||||
<option value="11">ESP32-POE-WROVER</option>
|
||||
<option value="7">KIT-VE</option>
|
||||
<option value="12">LILYGO T-POE Pro</option>
|
||||
<option value="4">QuinLED Uno/Quad</option>
|
||||
<option value="8">QuinLED Octa & T-ETH-POE</option>
|
||||
<option value="14">QuinLED v4 Uno/Quad</option>
|
||||
<option value="15">QuinLED v4 Octa</option>
|
||||
<option value="8">QuinLED-Dig-Octa & T-ETH-POE</option>
|
||||
<option value="4">QuinLED-ESP32</option>
|
||||
<option value="10">Serg74-ETH32</option>
|
||||
<option value="5">TwilightLord-ESP32</option>
|
||||
<option value="3">WESP32</option>
|
||||
|
||||
@@ -122,7 +122,7 @@ bool DMXInput::installDriver()
|
||||
return true;
|
||||
}
|
||||
|
||||
void DMXInput::init(int8_t rxPin, int8_t txPin, int8_t enPin, uint8_t inputPortNum)
|
||||
void DMXInput::init(uint8_t rxPin, uint8_t txPin, uint8_t enPin, uint8_t inputPortNum)
|
||||
{
|
||||
|
||||
#ifdef WLED_ENABLE_DMX_OUTPUT
|
||||
@@ -142,7 +142,7 @@ void DMXInput::init(int8_t rxPin, int8_t txPin, int8_t enPin, uint8_t inputPortN
|
||||
return;
|
||||
}
|
||||
|
||||
if (rxPin >= 0 && enPin >= 0 && txPin >= 0) {
|
||||
if (rxPin > 0 && enPin > 0 && txPin > 0) {
|
||||
|
||||
const managed_pin_type pins[] = {
|
||||
{(int8_t)txPin, false}, // these are not used as gpio pins, thus isOutput is always false.
|
||||
|
||||
+5
-5
@@ -12,7 +12,7 @@
|
||||
class DMXInput
|
||||
{
|
||||
public:
|
||||
void init(int8_t rxPin, int8_t txPin, int8_t enPin, uint8_t inputPortNum);
|
||||
void init(uint8_t rxPin, uint8_t txPin, uint8_t enPin, uint8_t inputPortNum);
|
||||
void update();
|
||||
|
||||
/**disable dmx receiver (do this before disabling the cache)*/
|
||||
@@ -54,9 +54,9 @@ private:
|
||||
friend void dmxReceiverTask(void * context);
|
||||
|
||||
uint8_t inputPortNum = 255;
|
||||
int8_t rxPin = -1;
|
||||
int8_t txPin = -1;
|
||||
int8_t enPin = -1;
|
||||
uint8_t rxPin = 255;
|
||||
uint8_t txPin = 255;
|
||||
uint8_t enPin = 255;
|
||||
|
||||
/// is written to by the dmx receive task.
|
||||
byte dmxdata[DMX_PACKET_SIZE];
|
||||
@@ -72,5 +72,5 @@ private:
|
||||
TaskHandle_t task;
|
||||
/// Guards access to dmxData
|
||||
std::mutex dmxDataLock;
|
||||
|
||||
|
||||
};
|
||||
|
||||
@@ -20,4 +20,15 @@ Macros for generating a "dynamic array", a static array of objects declared in d
|
||||
#define DYNARRAY_END(array_name) array_name##_end
|
||||
#define DYNARRAY_LENGTH(array_name) (&DYNARRAY_END(array_name)[0] - &DYNARRAY_BEGIN(array_name)[0])
|
||||
|
||||
#ifdef ESP8266
|
||||
// ESP8266 linker script cannot be extended with a unique section for dynamic arrays.
|
||||
// We instead pack them in the ".dtors" section, as it's sorted and uploaded to the flash
|
||||
// (but will never be used in the embedded system)
|
||||
#define DYNARRAY_SECTION ".dtors"
|
||||
|
||||
#else /* ESP8266 */
|
||||
|
||||
// Use a unique named section; the linker script must be extended to ensure it's correctly placed.
|
||||
#define DYNARRAY_SECTION ".dynarray"
|
||||
|
||||
#endif
|
||||
|
||||
+14
-33
@@ -5,7 +5,7 @@
|
||||
#define MAX_CHANNELS_PER_UNIVERSE 512
|
||||
|
||||
// forward declarations
|
||||
static void handleDDPPacket(e131_packet_t* p, size_t packetLen);
|
||||
static void handleDDPPacket(e131_packet_t* p);
|
||||
static void handleArtnetPollReply(IPAddress ipAddress);
|
||||
static void prepareArtnetPollReply(ArtPollReply *reply);
|
||||
static void sendArtnetPollReply(ArtPollReply *reply, IPAddress ipAddress, uint16_t portAddress);
|
||||
@@ -17,31 +17,19 @@ 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, size_t packetLen) {
|
||||
static void handleDDPPacket(e131_packet_t* p) {
|
||||
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;
|
||||
if (p->dataType != DDP_TYPE_RGB24 && p->dataType != 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 status and config packets (not implemented)
|
||||
if (p->destination == DDP_ID_STATUS || p->destination == DDP_ID_CONFIG) return;
|
||||
|
||||
// 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;
|
||||
|
||||
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, if more are used and packets are very late, they are still accepted)
|
||||
//reject late packets belonging to previous frame (assuming 4 packets max. before push)
|
||||
if (e131SkipOutOfSequence && lastPushSeq) {
|
||||
int sn = p->sequenceNum & 0xF; // sequence number is 4 bits, 1-15, 0 means unused
|
||||
int sn = p->sequenceNum & 0xF;
|
||||
if (sn) {
|
||||
if (lastPushSeq > 5) {
|
||||
if (sn > (lastPushSeq -5) && sn < lastPushSeq) return;
|
||||
@@ -51,8 +39,7 @@ static void handleDDPPacket(e131_packet_t* p, size_t packetLen) {
|
||||
}
|
||||
}
|
||||
|
||||
unsigned ddpChannelsPerLed = 3; // default to RGB
|
||||
if ((p->dataType & 0b00111000)>>3 == 0b011) ddpChannelsPerLed = 4; // RGBW data type (see DDP protocol definition)
|
||||
unsigned ddpChannelsPerLed = ((p->dataType & 0b00111000)>>3 == 0b011) ? 4 : 3; // data type 0x1B (formerly 0x1A) is RGBW (type 3, 8 bit/channel)
|
||||
|
||||
uint32_t start = htonl(p->channelOffset) / ddpChannelsPerLed;
|
||||
start += DMXAddress / ddpChannelsPerLed;
|
||||
@@ -62,14 +49,8 @@ static void handleDDPPacket(e131_packet_t* p, size_t packetLen) {
|
||||
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 = numLeds * ddpChannelsPerLed; // validate bounds before accessing data array
|
||||
unsigned maxDataIndex = c + numLeds * ddpChannelsPerLed; // validate bounds before accessing data array
|
||||
if (maxDataIndex > dataLen) {
|
||||
DEBUG_PRINTLN(F("DDP packet data bounds exceeded, rejecting."));
|
||||
return;
|
||||
@@ -84,6 +65,7 @@ static void handleDDPPacket(e131_packet_t* p, size_t packetLen) {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -93,7 +75,7 @@ static void handleDDPPacket(e131_packet_t* p, size_t packetLen) {
|
||||
}
|
||||
|
||||
//E1.31 and Art-Net protocol support
|
||||
void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol, size_t packetLen){
|
||||
void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol){
|
||||
|
||||
int uni = 0, dmxChannels = 0;
|
||||
uint8_t* e131_data = nullptr;
|
||||
@@ -101,7 +83,6 @@ void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol, size_
|
||||
|
||||
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;
|
||||
@@ -114,9 +95,9 @@ void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol, size_
|
||||
} 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; // on malformed packets, this can become negative, checked below
|
||||
dmxChannels = htons(p->property_value_count) - 1;
|
||||
// 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;
|
||||
@@ -128,7 +109,7 @@ void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol, size_
|
||||
}
|
||||
} else { //DDP
|
||||
realtimeIP = clientIP;
|
||||
handleDDPPacket(p, packetLen);
|
||||
handleDDPPacket(p);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -100,7 +100,7 @@ void initDMXInput();
|
||||
void handleDMXInput();
|
||||
|
||||
//e131.cpp
|
||||
void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol, size_t packetLen);
|
||||
void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol);
|
||||
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
|
||||
@@ -176,6 +176,7 @@ void serializeSegment(const JsonObject& root, const Segment& seg, byte id, bool
|
||||
void serializeState(JsonObject root, bool forPreset = false, bool includeBri = true, bool segmentBounds = true, bool selectedSegmentsOnly = false);
|
||||
void serializeInfo(JsonObject root);
|
||||
void serializeModeNames(JsonArray arr);
|
||||
void serializeModeData(JsonArray fxdata);
|
||||
void serializePins(JsonObject root);
|
||||
void serveJson(AsyncWebServerRequest* request);
|
||||
#ifdef WLED_ENABLE_JSONLIVE
|
||||
|
||||
+9
-3
@@ -188,6 +188,7 @@ static bool appendObjectToFile(const char* key, const JsonDocument* content, uin
|
||||
if (f.size() < 3) {
|
||||
char init[10];
|
||||
strcpy_P(init, PSTR("{\"0\":{}}"));
|
||||
f.seek(0, SeekSet); // rewind to ensure we overwrite from the start, instead of appending
|
||||
f.print(init);
|
||||
}
|
||||
|
||||
@@ -354,12 +355,17 @@ bool readObjectFromFile(const char* file, const char* key, JsonDocument* dest, c
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filter) deserializeJson(*dest, f, DeserializationOption::Filter(*filter));
|
||||
else deserializeJson(*dest, f);
|
||||
DeserializationError jsonErr = DeserializationError::Ok;
|
||||
if (filter) jsonErr = deserializeJson(*dest, f, DeserializationOption::Filter(*filter));
|
||||
else jsonErr = deserializeJson(*dest, f);
|
||||
|
||||
f.close();
|
||||
DEBUGFS_PRINTF("Read, took %lu ms\n", millis() - s);
|
||||
return true;
|
||||
|
||||
if (jsonErr) { // EmptyInput, IncompleteInput, InvalidInput, NoMemory, TooDeep
|
||||
DEBUG_PRINTF_P(PSTR("readObjectFromFile(%s): JSON %s !\n"), fileName, jsonErr.c_str());
|
||||
return jsonErr == DeserializationError::NoMemory || jsonErr == DeserializationError::EmptyInput; // NoMemory => data is partial but usable; empty => handled by caller
|
||||
} else return true;
|
||||
}
|
||||
|
||||
void updateFSInfo() {
|
||||
|
||||
+27
-106
@@ -761,7 +761,6 @@ void serializeInfo(JsonObject root)
|
||||
case REALTIME_MODE_ARTNET: root["lm"] = F("Art-Net"); break;
|
||||
case REALTIME_MODE_TPM2NET: root["lm"] = F("tpm2.net"); break;
|
||||
case REALTIME_MODE_DDP: root["lm"] = F("DDP"); break;
|
||||
case REALTIME_MODE_DMX: root["lm"] = F("DMX"); break;
|
||||
}
|
||||
|
||||
root[F("lip")] = realtimeIP[0] == 0 ? "" : realtimeIP.toString();
|
||||
@@ -774,18 +773,8 @@ void serializeInfo(JsonObject root)
|
||||
|
||||
root[F("fxcount")] = strip.getModeCount();
|
||||
root[F("palcount")] = getPaletteCount();
|
||||
root[F("cpalcount")] = customPalettes.size(); // number of user custom palettes (includes gray placeholders)
|
||||
root[F("umpalcount")] = usermodPalettes.size(); // number of usermod-registered palettes
|
||||
root[F("cpalcount")] = customPalettes.size(); // number of custom palettes
|
||||
root[F("cpalmax")] = WLED_MAX_CUSTOM_PALETTES; // maximum number of custom palettes
|
||||
// send usermod palette names so the UI can label them correctly
|
||||
if (usermodPalettes.size() > 0) {
|
||||
JsonArray umpalnames = root.createNestedArray(F("umpalnames"));
|
||||
for (size_t j = 0; j < usermodPalettes.size(); j++) {
|
||||
char buf[34];
|
||||
extractModeName(WLED_USERMOD_PALETTE_ID_BASE - j, JSON_palette_names, buf, sizeof(buf) - 1);
|
||||
umpalnames.add(buf);
|
||||
}
|
||||
}
|
||||
|
||||
JsonArray ledmaps = root.createNestedArray(F("maps"));
|
||||
for (size_t i=0; i<WLED_MAX_LEDMAPS; i++) {
|
||||
@@ -956,28 +945,19 @@ void serializePalettes(JsonObject root, int page)
|
||||
#endif
|
||||
|
||||
const int customPalettesCount = customPalettes.size();
|
||||
const int umPalettesCount = usermodPalettes.size();
|
||||
const int palettesCount = FIXED_PALETTE_COUNT; // palettesCount is number of palettes, not palette index
|
||||
|
||||
const int maxPage = (palettesCount + umPalettesCount + customPalettesCount) / itemPerPage;
|
||||
const int maxPage = (palettesCount + customPalettesCount) / itemPerPage;
|
||||
if (page > maxPage) page = maxPage;
|
||||
|
||||
const int start = itemPerPage * page;
|
||||
int end = min(start + itemPerPage, palettesCount + umPalettesCount + customPalettesCount);
|
||||
int end = min(start + itemPerPage, palettesCount + customPalettesCount);
|
||||
|
||||
root[F("m")] = maxPage; // inform caller how many pages there are
|
||||
JsonObject palettes = root.createNestedObject("p");
|
||||
|
||||
for (int i = start; i < end; i++) {
|
||||
// compute the palette ID for this sequential index
|
||||
int paletteId;
|
||||
if (i >= palettesCount + umPalettesCount) // user custom palette (IDs 200, 199, ...)
|
||||
paletteId = WLED_CUSTOM_PALETTE_ID_BASE - (i - palettesCount - umPalettesCount);
|
||||
else if (i >= palettesCount) // usermod palette (IDs 255, 254, ...)
|
||||
paletteId = WLED_USERMOD_PALETTE_ID_BASE - (i - palettesCount);
|
||||
else
|
||||
paletteId = i; // fixed palette
|
||||
JsonArray curPalette = palettes.createNestedArray(String(paletteId));
|
||||
JsonArray curPalette = palettes.createNestedArray(String(i >= palettesCount ? 255 - i + palettesCount : i));
|
||||
switch (i) {
|
||||
case 0: //default palette
|
||||
setPaletteColors(curPalette, PartyColors_gc22);
|
||||
@@ -1006,13 +986,9 @@ void serializePalettes(JsonObject root, int page)
|
||||
curPalette.add("c1");
|
||||
break;
|
||||
default:
|
||||
if (i >= palettesCount + umPalettesCount) { // user custom palettes (lowest IDs in the custom range)
|
||||
int custIdx = i - palettesCount - umPalettesCount;
|
||||
setPaletteColors(curPalette, customPalettes[custIdx]);
|
||||
} else if (i >= palettesCount) { // usermod palettes (IDs 255, 254, ...)
|
||||
int umIdx = i - palettesCount;
|
||||
setPaletteColors(curPalette, usermodPalettes[umIdx].palette);
|
||||
} else if (i < DYNAMIC_PALETTE_COUNT + FASTLED_PALETTE_COUNT) // palette 6 - 12, fastled palettes
|
||||
if (i >= palettesCount) // custom palettes
|
||||
setPaletteColors(curPalette, customPalettes[i - palettesCount]);
|
||||
else if (i < DYNAMIC_PALETTE_COUNT + FASTLED_PALETTE_COUNT) // palette 6 - 12, fastled palettes
|
||||
setPaletteColors(curPalette, *fastledPalettes[i - DYNAMIC_PALETTE_COUNT]);
|
||||
else {
|
||||
memcpy_P(tcp, (byte*)pgm_read_dword(&(gGradientPalettes[i - (DYNAMIC_PALETTE_COUNT + FASTLED_PALETTE_COUNT)])), sizeof(tcp));
|
||||
@@ -1184,6 +1160,21 @@ void serializePins(JsonObject root)
|
||||
}
|
||||
}
|
||||
|
||||
// deserializes mode data string into JsonArray
|
||||
void serializeModeData(JsonArray fxdata)
|
||||
{
|
||||
char lineBuffer[256];
|
||||
for (size_t i = 0; i < strip.getModeCount(); i++) {
|
||||
strncpy_P(lineBuffer, strip.getModeData(i), sizeof(lineBuffer)/sizeof(char)-1);
|
||||
lineBuffer[sizeof(lineBuffer)/sizeof(char)-1] = '\0'; // terminate string
|
||||
if (lineBuffer[0] != 0) {
|
||||
char* dataPtr = strchr(lineBuffer,'@');
|
||||
if (dataPtr) fxdata.add(dataPtr+1);
|
||||
else fxdata.add("");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// deserializes mode names string into JsonArray
|
||||
// also removes effect data extensions (@...) from deserialised names
|
||||
void serializeModeNames(JsonArray arr)
|
||||
@@ -1200,78 +1191,6 @@ void serializeModeNames(JsonArray arr)
|
||||
}
|
||||
}
|
||||
|
||||
// Writes a JSON-escaped string (with surrounding quotes) into dest[0..maxLen-1].
|
||||
// Returns bytes written, or 0 if the buffer was too small.
|
||||
static size_t writeJSONString(uint8_t* dest, size_t maxLen, const char* src) {
|
||||
size_t pos = 0;
|
||||
|
||||
auto emit = [&](char c) -> bool {
|
||||
if (pos >= maxLen) return false;
|
||||
dest[pos++] = (uint8_t)c;
|
||||
return true;
|
||||
};
|
||||
|
||||
if (!emit('"')) return 0;
|
||||
|
||||
for (const char* p = src; *p; ++p) {
|
||||
char esc = ARDUINOJSON_NAMESPACE::EscapeSequence::escapeChar(*p);
|
||||
if (esc) {
|
||||
if (!emit('\\') || !emit(esc)) return 0;
|
||||
} else {
|
||||
if (!emit(*p)) return 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (!emit('"')) return 0;
|
||||
return pos;
|
||||
}
|
||||
|
||||
// Writes ,"<escaped_src>" into dest[0..maxLen-1] (no null terminator).
|
||||
// Returns bytes written, or 0 if the buffer was too small.
|
||||
static size_t writeJSONStringElement(uint8_t* dest, size_t maxLen, const char* src) {
|
||||
if (maxLen == 0) return 0;
|
||||
dest[0] = ',';
|
||||
size_t n = writeJSONString(dest + 1, maxLen - 1, src);
|
||||
if (n == 0) return 0;
|
||||
return 1 + n;
|
||||
}
|
||||
|
||||
// Generate a streamed JSON response for the mode data
|
||||
// This uses sendChunked to send the reply in blocks based on how much fit in the outbound
|
||||
// packet buffer, minimizing the required state (ie. just the next index to send). This
|
||||
// allows us to send an arbitrarily large response without using any significant amount of
|
||||
// memory (so no worries about buffer limits).
|
||||
void respondModeData(AsyncWebServerRequest* request) {
|
||||
size_t fx_index = 0;
|
||||
request->sendChunked(FPSTR(CONTENT_TYPE_JSON),
|
||||
[fx_index](uint8_t* data, size_t len, size_t) mutable {
|
||||
size_t bytes_written = 0;
|
||||
char lineBuffer[256];
|
||||
while (fx_index < strip.getModeCount()) {
|
||||
strncpy_P(lineBuffer, strip.getModeData(fx_index), sizeof(lineBuffer)-1); // Copy to stack buffer for strchr
|
||||
if (lineBuffer[0] != 0) {
|
||||
lineBuffer[sizeof(lineBuffer)-1] = '\0'; // terminate string (only needed if strncpy filled the buffer)
|
||||
const char* dataPtr = strchr(lineBuffer,'@'); // Find '@', if there is one
|
||||
size_t mode_bytes = writeJSONStringElement(data, len, dataPtr ? dataPtr + 1 : "");
|
||||
if (mode_bytes == 0) break; // didn't fit; break loop and try again next packet
|
||||
if (fx_index == 0) *data = '[';
|
||||
data += mode_bytes;
|
||||
len -= mode_bytes;
|
||||
bytes_written += mode_bytes;
|
||||
}
|
||||
++fx_index;
|
||||
}
|
||||
|
||||
if ((fx_index == strip.getModeCount()) && (len >= 1)) {
|
||||
*data = ']';
|
||||
++bytes_written;
|
||||
++fx_index; // we're really done
|
||||
}
|
||||
|
||||
return bytes_written;
|
||||
});
|
||||
}
|
||||
|
||||
// Global buffer locking response helper class (to make sure lock is released when AsyncJsonResponse is destroyed)
|
||||
class LockedJsonResponse: public AsyncJsonResponse {
|
||||
bool _holding_lock;
|
||||
@@ -1299,7 +1218,7 @@ class LockedJsonResponse: public AsyncJsonResponse {
|
||||
void serveJson(AsyncWebServerRequest* request)
|
||||
{
|
||||
enum class json_target {
|
||||
all, state, info, state_info, nodes, effects, palettes, networks, config, pins
|
||||
all, state, info, state_info, nodes, effects, palettes, fxdata, networks, config, pins
|
||||
};
|
||||
json_target subJson = json_target::all;
|
||||
|
||||
@@ -1310,7 +1229,7 @@ void serveJson(AsyncWebServerRequest* request)
|
||||
else if (url.indexOf(F("nodes")) > 0) subJson = json_target::nodes;
|
||||
else if (url.indexOf(F("eff")) > 0) subJson = json_target::effects;
|
||||
else if (url.indexOf(F("palx")) > 0) subJson = json_target::palettes;
|
||||
else if (url.indexOf(F("fxda")) > 0) { respondModeData(request); return; }
|
||||
else if (url.indexOf(F("fxda")) > 0) subJson = json_target::fxdata;
|
||||
else if (url.indexOf(F("net")) > 0) subJson = json_target::networks;
|
||||
else if (url.indexOf(F("cfg")) > 0) subJson = json_target::config;
|
||||
else if (url.indexOf(F("pins")) > 0) subJson = json_target::pins;
|
||||
@@ -1335,7 +1254,7 @@ void serveJson(AsyncWebServerRequest* request)
|
||||
}
|
||||
// releaseJSONBufferLock() will be called when "response" is destroyed (from AsyncWebServer)
|
||||
// make sure you delete "response" if no "request->send(response);" is made
|
||||
LockedJsonResponse *response = new LockedJsonResponse(pDoc, subJson==json_target::effects); // will clear and convert JsonDocument into JsonArray if necessary
|
||||
LockedJsonResponse *response = new LockedJsonResponse(pDoc, subJson==json_target::fxdata || subJson==json_target::effects); // will clear and convert JsonDocument into JsonArray if necessary
|
||||
|
||||
JsonVariant lDoc = response->getRoot();
|
||||
|
||||
@@ -1351,6 +1270,8 @@ void serveJson(AsyncWebServerRequest* request)
|
||||
serializePalettes(lDoc, request->hasParam(F("page")) ? request->getParam(F("page"))->value().toInt() : 0); break;
|
||||
case json_target::effects:
|
||||
serializeModeNames(lDoc); break;
|
||||
case json_target::fxdata:
|
||||
serializeModeData(lDoc); break;
|
||||
case json_target::networks:
|
||||
serializeNetworks(lDoc); break;
|
||||
case json_target::config:
|
||||
|
||||
@@ -155,26 +155,6 @@ const ethernet_settings ethernetBoards[] = {
|
||||
ETH_PHY_LAN8720, // eth_type,
|
||||
ETH_CLOCK_GPIO0_IN // eth_clk_mode
|
||||
},
|
||||
|
||||
// WLED_ETH_QUINLED_V4 (14) - QuinLED Dig-Uno/Quad v4
|
||||
{
|
||||
0, // eth_address
|
||||
-1, // eth_power
|
||||
7, // eth_mdc
|
||||
8, // eth_mdio
|
||||
ETH_PHY_LAN8720, // eth_type
|
||||
ETH_CLOCK_GPIO0_IN // eth_clk_mode
|
||||
},
|
||||
|
||||
// WLED_ETH_QUINLED_OCTA_V4 (15) - QuinLED Dig-Octa 32-8L v4
|
||||
{
|
||||
0, // eth_address
|
||||
-1, // eth_power
|
||||
23, // eth_mdc
|
||||
18, // eth_mdio
|
||||
ETH_PHY_LAN8720, // eth_type
|
||||
ETH_CLOCK_GPIO0_IN // eth_clk_mode
|
||||
},
|
||||
};
|
||||
|
||||
bool initEthernet()
|
||||
|
||||
+79
-222
@@ -6,8 +6,10 @@
|
||||
#include <esp_ota_ops.h>
|
||||
#include <esp_flash.h>
|
||||
#include <mbedtls/sha256.h>
|
||||
#endif
|
||||
|
||||
// Platform-specific metadata locations
|
||||
#ifdef ESP32
|
||||
constexpr size_t METADATA_OFFSET = 256; // ESP32: metadata appears after Espressif metadata
|
||||
#define UPDATE_ERROR errorString
|
||||
|
||||
@@ -29,43 +31,16 @@ constexpr size_t BOOTLOADER_SIZE = 0x8000; // 32KB, typical bootloader size
|
||||
#elif defined(ESP8266)
|
||||
constexpr size_t METADATA_OFFSET = 0x1000; // ESP8266: metadata appears at 4KB offset
|
||||
#define UPDATE_ERROR getErrorString
|
||||
#define SUPPORT_GZIPPED_OTA
|
||||
#endif
|
||||
|
||||
#ifdef SUPPORT_GZIPPED_OTA
|
||||
#include <uzlib.h> // ArduinoUZlib library: gzipped firmware uploads are decoded by eboot at boot,
|
||||
// so the upload stream itself can arrive compressed; we decompress just enough
|
||||
// to find the metadata for OTA validation.
|
||||
#endif
|
||||
|
||||
constexpr size_t METADATA_SEARCH_RANGE = 512; // bytes
|
||||
|
||||
// State structure for update process
|
||||
namespace {
|
||||
struct UpdateContext {
|
||||
// State flags
|
||||
// FUTURE: the flags could be replaced by a state machine
|
||||
bool replySent = false;
|
||||
bool needsRestart = false;
|
||||
bool updateStarted = false;
|
||||
bool uploadComplete = false;
|
||||
bool releaseCheckPassed = false;
|
||||
String errorMessage;
|
||||
|
||||
// Buffer to hold block data across posts, if needed
|
||||
std::vector<uint8_t> releaseMetadataBuffer;
|
||||
|
||||
#ifdef SUPPORT_GZIPPED_OTA
|
||||
bool gzipDetected = false; // upload is a gzip stream (eboot will decompress at boot)
|
||||
#endif
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if OTA should be allowed based on release compatibility using custom description
|
||||
* @param binaryData Pointer to binary file data (not modified)
|
||||
* @param dataSize Size of binary data in bytes
|
||||
* @param errorMessage Buffer to store error message if validation fails
|
||||
* @param errorMessage Buffer to store error message if validation fails
|
||||
* @param errorMessageLen Maximum length of error message buffer
|
||||
* @return true if OTA should proceed, false if it should be blocked
|
||||
*/
|
||||
@@ -92,104 +67,20 @@ static bool validateOTA(const uint8_t* binaryData, size_t dataSize, char* errorM
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef SUPPORT_GZIPPED_OTA
|
||||
constexpr size_t GZIP_DECOMPRESSED_SIZE = METADATA_OFFSET + METADATA_SEARCH_RANGE;
|
||||
// We use the JSON buffer to hold the decompressed metadata, so we need to ensure it's large enough
|
||||
static_assert(JSON_BUFFER_SIZE >= GZIP_DECOMPRESSED_SIZE, "JSON_BUFFER_SIZE must be at least GZIP_DECOMPRESSED_SIZE to support gzipped OTA updates");
|
||||
struct UpdateContext {
|
||||
// State flags
|
||||
// FUTURE: the flags could be replaced by a state machine
|
||||
bool replySent = false;
|
||||
bool needsRestart = false;
|
||||
bool updateStarted = false;
|
||||
bool uploadComplete = false;
|
||||
bool releaseCheckPassed = false;
|
||||
String errorMessage;
|
||||
|
||||
// File-static flash source context for the uzlib source_read_cb.
|
||||
// The 256-byte read buffer must be 4-byte aligned for ESP.flashRead().
|
||||
static struct {
|
||||
uint32_t flashBase;
|
||||
uint32_t flashOffset;
|
||||
uint8_t buf[256] __attribute__((aligned(4)));
|
||||
} s_gzipFlashSrc;
|
||||
// Buffer to hold block data across posts, if needed
|
||||
std::vector<uint8_t> releaseMetadataBuffer;
|
||||
};
|
||||
|
||||
static int gzip_flash_read_cb(struct uzlib_uncomp* d)
|
||||
{
|
||||
if (!ESP.flashRead(s_gzipFlashSrc.flashBase + s_gzipFlashSrc.flashOffset,
|
||||
reinterpret_cast<uint32_t*>(s_gzipFlashSrc.buf),
|
||||
sizeof(s_gzipFlashSrc.buf))) {
|
||||
return -1;
|
||||
}
|
||||
s_gzipFlashSrc.flashOffset += sizeof(s_gzipFlashSrc.buf);
|
||||
d->source = s_gzipFlashSrc.buf;
|
||||
d->source_limit = s_gzipFlashSrc.buf + sizeof(s_gzipFlashSrc.buf);
|
||||
return *d->source++;
|
||||
}
|
||||
|
||||
static bool unzipFlash(uint32_t flashBase, uint8_t* outBuf, char* errorMessage, size_t errorMessageLen)
|
||||
{
|
||||
uzlib_uncomp uncomp_ctx; // large, but we can get away with this since we're on the system stack
|
||||
|
||||
s_gzipFlashSrc.flashBase = flashBase;
|
||||
s_gzipFlashSrc.flashOffset = 0;
|
||||
uncomp_ctx.source = s_gzipFlashSrc.buf;
|
||||
uncomp_ctx.source_limit = s_gzipFlashSrc.buf; // empty: first byte triggers callback
|
||||
uncomp_ctx.source_read_cb = gzip_flash_read_cb;
|
||||
uzlib_uncompress_init(&uncomp_ctx, nullptr, 0);
|
||||
|
||||
bool ok = false;
|
||||
if (uzlib_gzip_parse_header(&uncomp_ctx) != TINF_OK) {
|
||||
strncpy_P(errorMessage, PSTR("Invalid gzip header in OTA firmware"), errorMessageLen - 1);
|
||||
errorMessage[errorMessageLen - 1] = '\0';
|
||||
} else {
|
||||
uncomp_ctx.dest_start = uncomp_ctx.dest = outBuf;
|
||||
uncomp_ctx.dest_limit = outBuf + GZIP_DECOMPRESSED_SIZE;
|
||||
int res = TINF_OK;
|
||||
while (res == TINF_OK && uncomp_ctx.dest < uncomp_ctx.dest_limit) res = uzlib_uncompress(&uncomp_ctx);
|
||||
if (uncomp_ctx.dest >= uncomp_ctx.dest_limit) {
|
||||
ok = true;
|
||||
} else {
|
||||
strncpy_P(errorMessage, PSTR("Not enough decompressed data for validation"), errorMessageLen - 1);
|
||||
errorMessage[errorMessageLen - 1] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
return ok;
|
||||
}
|
||||
|
||||
static void validateGzippedOTA(UpdateContext* context) {
|
||||
// Compute the flash address to read from based on the expected update size and the location of the FS partition, and then decompress just enough of the firmware to find the metadata for validation.
|
||||
extern uint32_t _FS_start;
|
||||
const size_t updateSize = Update.size();
|
||||
// Logic borrowed from ESP8266 Updater.cpp
|
||||
const size_t roundedSize = (updateSize + FLASH_SECTOR_SIZE - 1) & ~(FLASH_SECTOR_SIZE - 1);
|
||||
const uintptr_t fsPhysAddr = (uintptr_t)&_FS_start - 0x40200000;
|
||||
const uint32_t flashBase = (fsPhysAddr > roundedSize) ? (fsPhysAddr - roundedSize) : 0;
|
||||
uint8_t* compressionBuf = static_cast<uint8_t*>(pDoc->memoryPool().buffer()); // borrow JSON buffer
|
||||
|
||||
char errorMessage[128];
|
||||
bool ok = unzipFlash(flashBase, compressionBuf, errorMessage, sizeof(errorMessage));
|
||||
if (!ok) {
|
||||
DEBUG_PRINTF_P(PSTR("OTA unzip failed: %s\n"), errorMessage);
|
||||
context->errorMessage = errorMessage;
|
||||
return;
|
||||
}
|
||||
|
||||
ok = validateOTA(compressionBuf, GZIP_DECOMPRESSED_SIZE, errorMessage, sizeof(errorMessage));
|
||||
if (!ok) {
|
||||
DEBUG_PRINTF_P(PSTR("OTA declined: %s\n"), errorMessage);
|
||||
context->errorMessage = errorMessage;
|
||||
context->errorMessage += F(" Enable 'Ignore firmware validation' to proceed anyway.");
|
||||
return;
|
||||
}
|
||||
|
||||
DEBUG_PRINTLN(F("OTA allowed: Release compatibility check passed (gzipped)"));
|
||||
context->releaseCheckPassed = true;
|
||||
|
||||
// Write the final block
|
||||
if (!Update.hasError()) {
|
||||
const auto& buf = context->releaseMetadataBuffer;
|
||||
if (Update.write(const_cast<uint8_t*>(buf.data()), buf.size()) != buf.size()) {
|
||||
DEBUG_PRINTF_P(PSTR("OTA write failed on final chunk: %s\n"), Update.UPDATE_ERROR());
|
||||
}
|
||||
}
|
||||
|
||||
DEBUG_PRINTLN(F("OTA Update End")); // for symmetry with the non-gzip path
|
||||
context->uploadComplete = true; // Final block was passed to Update.write()
|
||||
}
|
||||
#endif
|
||||
|
||||
static void endOTA(AsyncWebServerRequest *request) {
|
||||
UpdateContext* context = reinterpret_cast<UpdateContext*>(request->_tempObject);
|
||||
@@ -225,7 +116,7 @@ static bool beginOTA(AsyncWebServerRequest *request, UpdateContext* context)
|
||||
{
|
||||
#ifdef ESP8266
|
||||
Update.runAsync(true);
|
||||
#endif
|
||||
#endif
|
||||
|
||||
if (Update.isRunning()) {
|
||||
request->send(503);
|
||||
@@ -237,7 +128,7 @@ static bool beginOTA(AsyncWebServerRequest *request, UpdateContext* context)
|
||||
WLED::instance().disableWatchdog();
|
||||
#endif
|
||||
UsermodManager::onUpdateBegin(true); // notify usermods that update is about to begin (some may require task de-init)
|
||||
|
||||
|
||||
strip.suspend();
|
||||
backupConfig(); // backup current config in case the update ends badly
|
||||
strip.resetSegments(); // free as much memory as you can
|
||||
@@ -250,15 +141,15 @@ static bool beginOTA(AsyncWebServerRequest *request, UpdateContext* context)
|
||||
context->releaseCheckPassed = true;
|
||||
DEBUG_PRINTLN(F("OTA validation skipped by user"));
|
||||
}
|
||||
|
||||
|
||||
// Begin update with the firmware size from content length
|
||||
size_t updateSize = request->contentLength() > 0 ? request->contentLength() : ((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000);
|
||||
if (!Update.begin(updateSize)) {
|
||||
if (!Update.begin(updateSize)) {
|
||||
context->errorMessage = Update.UPDATE_ERROR();
|
||||
DEBUG_PRINTF_P(PSTR("OTA Failed to begin: %s\n"), context->errorMessage.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
context->updateStarted = true;
|
||||
return true;
|
||||
}
|
||||
@@ -267,7 +158,7 @@ static bool beginOTA(AsyncWebServerRequest *request, UpdateContext* context)
|
||||
// Returns true if successful, false on failure.
|
||||
bool initOTA(AsyncWebServerRequest *request) {
|
||||
// Allocate update context
|
||||
UpdateContext* context = new (std::nothrow) UpdateContext {};
|
||||
UpdateContext* context = new (std::nothrow) UpdateContext {};
|
||||
if (context) {
|
||||
request->_tempObject = context;
|
||||
request->onDisconnect([=]() { endOTA(request); }); // ensures we restart on failure
|
||||
@@ -283,43 +174,29 @@ void setOTAReplied(AsyncWebServerRequest *request) {
|
||||
context->replySent = true;
|
||||
};
|
||||
|
||||
// Returns the OTA outcome as an OTAResultStatus plus an optional error string.
|
||||
// TryAgain: JSON lock busy; caller must call deferResponse() and retry.
|
||||
// Replied: response already sent; no action needed.
|
||||
// Ready: result available; empty string = success (200), non-empty = failure (500).
|
||||
std::pair<OTAResultStatus, String> getOTAResult(AsyncWebServerRequest* request) {
|
||||
// Returns pointer to error message, or nullptr if OTA was successful.
|
||||
std::pair<bool, String> getOTAResult(AsyncWebServerRequest* request) {
|
||||
UpdateContext* context = reinterpret_cast<UpdateContext*>(request->_tempObject);
|
||||
if (!context) return { OTAResultStatus::Ready, F("OTA context unexpectedly missing") };
|
||||
if (context->replySent) return { OTAResultStatus::Replied, {} };
|
||||
|
||||
#ifdef SUPPORT_GZIPPED_OTA
|
||||
if (context->gzipDetected && !context->errorMessage.length()) {
|
||||
JSONBufferGuard jsonGuard(JSON_LOCK_OTA);
|
||||
if (!jsonGuard) return { OTAResultStatus::TryAgain, {} }; // busy — caller will deferResponse()
|
||||
validateGzippedOTA(context); // Stores error in context->errorMessage if there's a problem
|
||||
context->gzipDetected = false; // all done
|
||||
}
|
||||
#endif
|
||||
|
||||
if (context->errorMessage.length()) return { OTAResultStatus::Ready, context->errorMessage };
|
||||
if (!context) return { true, F("OTA context unexpectedly missing") };
|
||||
if (context->replySent) return { false, {} };
|
||||
if (context->errorMessage.length()) return { true, context->errorMessage };
|
||||
|
||||
if (context->updateStarted) {
|
||||
// Release the OTA context now.
|
||||
endOTA(request);
|
||||
if (Update.hasError()) {
|
||||
return { OTAResultStatus::Ready, Update.UPDATE_ERROR() };
|
||||
return { true, Update.UPDATE_ERROR() };
|
||||
} else {
|
||||
return { OTAResultStatus::Ready, {} };
|
||||
return { true, {} };
|
||||
}
|
||||
}
|
||||
|
||||
// Should never happen
|
||||
return { OTAResultStatus::Ready, F("Internal software failure") };
|
||||
return { true, F("Internal software failure") };
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
void handleOTAData(AsyncWebServerRequest *request, size_t index, uint8_t *data, size_t len, bool isFinal)
|
||||
{
|
||||
UpdateContext* context = reinterpret_cast<UpdateContext*>(request->_tempObject);
|
||||
@@ -331,89 +208,69 @@ void handleOTAData(AsyncWebServerRequest *request, size_t index, uint8_t *data,
|
||||
|
||||
if (index == 0) {
|
||||
if (!beginOTA(request, context)) return;
|
||||
|
||||
#ifdef SUPPORT_GZIPPED_OTA
|
||||
if (len >= 2 && data[0] == 0x1F && data[1] == 0x8B) {
|
||||
DEBUG_PRINTLN(F("OTA: gzipped firmware detected"));
|
||||
context->gzipDetected = true;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// Perform plain-firmware validation as data arrives.
|
||||
// Gzip validation is deferred to the end of the upload; skip this path for gzip.
|
||||
if (!context->releaseCheckPassed
|
||||
#ifdef SUPPORT_GZIPPED_OTA
|
||||
&& !context->gzipDetected
|
||||
#endif
|
||||
) {
|
||||
if ((index+len) > METADATA_OFFSET) {
|
||||
// Current chunk contains the metadata offset
|
||||
size_t availableDataAfterOffset = (index + len) - METADATA_OFFSET;
|
||||
// Perform validation if we haven't done it yet and we have reached the metadata offset
|
||||
if (!context->releaseCheckPassed && (index+len) > METADATA_OFFSET) {
|
||||
// Current chunk contains the metadata offset
|
||||
size_t availableDataAfterOffset = (index + len) - METADATA_OFFSET;
|
||||
|
||||
DEBUG_PRINTF_P(PSTR("OTA metadata check: %d in buffer, %d received, %d available\n"), context->releaseMetadataBuffer.size(), len, availableDataAfterOffset);
|
||||
DEBUG_PRINTF_P(PSTR("OTA metadata check: %d in buffer, %d received, %d available\n"), context->releaseMetadataBuffer.size(), len, availableDataAfterOffset);
|
||||
|
||||
if (availableDataAfterOffset >= METADATA_SEARCH_RANGE) {
|
||||
// We have enough data to validate, one way or another
|
||||
const uint8_t* search_data = data;
|
||||
size_t search_len = len;
|
||||
|
||||
// If we have saved data, use that instead
|
||||
if (context->releaseMetadataBuffer.size()) {
|
||||
// Add this data
|
||||
context->releaseMetadataBuffer.insert(context->releaseMetadataBuffer.end(), data, data+len);
|
||||
search_data = context->releaseMetadataBuffer.data();
|
||||
search_len = context->releaseMetadataBuffer.size();
|
||||
}
|
||||
|
||||
// Do the checking
|
||||
char errorMessage[128];
|
||||
bool OTA_ok = validateOTA(search_data, search_len, errorMessage, sizeof(errorMessage));
|
||||
|
||||
// Release buffer if there was one
|
||||
context->releaseMetadataBuffer = decltype(context->releaseMetadataBuffer){};
|
||||
|
||||
if (!OTA_ok) {
|
||||
DEBUG_PRINTF_P(PSTR("OTA declined: %s\n"), errorMessage);
|
||||
context->errorMessage = errorMessage;
|
||||
context->errorMessage += F(" Enable 'Ignore firmware validation' to proceed anyway.");
|
||||
return;
|
||||
} else {
|
||||
DEBUG_PRINTLN(F("OTA allowed: Release compatibility check passed"));
|
||||
context->releaseCheckPassed = true;
|
||||
}
|
||||
} else {
|
||||
// Store the data we just got for next pass
|
||||
if (availableDataAfterOffset >= METADATA_SEARCH_RANGE) {
|
||||
// We have enough data to validate, one way or another
|
||||
const uint8_t* search_data = data;
|
||||
size_t search_len = len;
|
||||
|
||||
// If we have saved data, use that instead
|
||||
if (context->releaseMetadataBuffer.size()) {
|
||||
// Add this data
|
||||
context->releaseMetadataBuffer.insert(context->releaseMetadataBuffer.end(), data, data+len);
|
||||
search_data = context->releaseMetadataBuffer.data();
|
||||
search_len = context->releaseMetadataBuffer.size();
|
||||
}
|
||||
|
||||
// Do the checking
|
||||
char errorMessage[128];
|
||||
bool OTA_ok = validateOTA(search_data, search_len, errorMessage, sizeof(errorMessage));
|
||||
|
||||
// Release buffer if there was one
|
||||
context->releaseMetadataBuffer = decltype(context->releaseMetadataBuffer){};
|
||||
|
||||
if (!OTA_ok) {
|
||||
DEBUG_PRINTF_P(PSTR("OTA declined: %s\n"), errorMessage);
|
||||
context->errorMessage = errorMessage;
|
||||
context->errorMessage += F(" Enable 'Ignore firmware validation' to proceed anyway.");
|
||||
return;
|
||||
} else {
|
||||
DEBUG_PRINTLN(F("OTA allowed: Release compatibility check passed"));
|
||||
context->releaseCheckPassed = true;
|
||||
}
|
||||
} else {
|
||||
// Store the data we just got for next pass
|
||||
context->releaseMetadataBuffer.insert(context->releaseMetadataBuffer.end(), data, data+len);
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef SUPPORT_GZIPPED_OTA
|
||||
// Gzip: stash the final chunk and defer validation to getOTAResult(), where
|
||||
// deferResponse() can be used if the JSON lock is not yet available.
|
||||
if (isFinal && context->gzipDetected && !context->releaseCheckPassed) {
|
||||
context->releaseMetadataBuffer.assign(data, data + len);
|
||||
return; // getOTAResult() will validate, write this chunk, and complete the update
|
||||
}
|
||||
#endif
|
||||
|
||||
// Check if validation was still pending (shouldn't happen normally)
|
||||
// This is done before writing the last chunk, so endOTA can abort
|
||||
// This is done before writing the last chunk, so endOTA can abort
|
||||
if (isFinal && !context->releaseCheckPassed) {
|
||||
DEBUG_PRINTLN(F("OTA failed: Validation never completed"));
|
||||
// Don't write the last chunk to the updater: this will trip an error later
|
||||
context->errorMessage = F("Release check data never arrived?");
|
||||
return;
|
||||
}
|
||||
|
||||
// Write chunk data to OTA update (only if release check passed or still pending)
|
||||
if (!Update.hasError()) {
|
||||
if (Update.write(data, len) != len) {
|
||||
DEBUG_PRINTF_P(PSTR("OTA write failed on chunk %zu: %s\n"), index, Update.UPDATE_ERROR());
|
||||
}
|
||||
}
|
||||
|
||||
if (isFinal) {
|
||||
if(isFinal) {
|
||||
DEBUG_PRINTLN(F("OTA Update End"));
|
||||
// Upload complete
|
||||
context->uploadComplete = true;
|
||||
}
|
||||
}
|
||||
@@ -474,7 +331,7 @@ public:
|
||||
return true; // needs more bytes for the header
|
||||
}
|
||||
|
||||
//DEBUG_PRINTF("BLS parsed segment [%08X %08X=%d], segment count %d, is %d\n", segmentHeader.load_addr, segmentHeader.data_len, segmentHeader.data_len, segmentsLeft, imageSize);
|
||||
//DEBUG_PRINTF("BLS parsed segment [%08X %08X=%d], segment count %d, is %d\n", segmentHeader.load_addr, segmentHeader.data_len, segmentHeader.data_len, segmentsLeft, imageSize);
|
||||
|
||||
// Validate segment size
|
||||
if (segmentHeader.data_len > BOOTLOADER_SIZE) {
|
||||
@@ -488,16 +345,16 @@ public:
|
||||
--segmentsLeft;
|
||||
if (segmentsLeft == 0) {
|
||||
// all done, actually; we don't need to read any more
|
||||
|
||||
|
||||
// Round up to nearest 16 bytes.
|
||||
// Always add 1 to account for the checksum byte.
|
||||
imageSize = ((imageSize/ 16) + 1) * 16;
|
||||
|
||||
//DEBUG_PRINTF("BLS complete, is %d\n", imageSize);
|
||||
//DEBUG_PRINTF("BLS complete, is %d\n", imageSize);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// If we don't have enough bytes ...
|
||||
if (len < segmentHeader.data_len) {
|
||||
//DEBUG_PRINTF("Needs more bytes\n");
|
||||
@@ -536,12 +393,12 @@ static uint8_t bootloaderSHA256Cache[32];
|
||||
/**
|
||||
* Calculate and cache the bootloader SHA256 digest
|
||||
* Reads the bootloader from flash and computes SHA256 hash
|
||||
*
|
||||
* Strictly speaking, most bootloader images already contain a hash at the end of the image;
|
||||
*
|
||||
* Strictly speaking, most bootloader images already contain a hash at the end of the image;
|
||||
* we could in theory just read it. The trouble is that we have to parse the structure anyways
|
||||
* to find the actual endpoint, so we might as well always calculate it ourselves rather than
|
||||
* handle a special case if the hash isn't stored.
|
||||
*
|
||||
*
|
||||
*/
|
||||
static void calculateBootloaderSHA256() {
|
||||
// Calculate SHA256
|
||||
@@ -680,7 +537,7 @@ static bool verifyBootloaderImage(const uint8_t* &buffer, size_t &len, String& b
|
||||
if (imageHeader.hash_appended == 1) {
|
||||
actualBootloaderSize += 32;
|
||||
}
|
||||
|
||||
|
||||
if (actualBootloaderSize > len) {
|
||||
// Same as above
|
||||
bootloaderErrorMsg = "Too small";
|
||||
@@ -778,7 +635,7 @@ bool initBootloaderOTA(AsyncWebServerRequest *request) {
|
||||
|
||||
context->bytesBuffered = 0;
|
||||
return true;
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
// Set bootloader OTA replied flag
|
||||
|
||||
+2
-9
@@ -1,7 +1,6 @@
|
||||
// WLED OTA update interface
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
#ifdef ESP8266
|
||||
#include <Updater.h>
|
||||
#else
|
||||
@@ -33,18 +32,12 @@ bool initOTA(AsyncWebServerRequest *request);
|
||||
*/
|
||||
void setOTAReplied(AsyncWebServerRequest *request);
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve the OTA result.
|
||||
* @param request Pointer to web request object
|
||||
* @return OTAResultStatus indicating result state; string with error message if the update failed.
|
||||
* @return bool indicating if a reply is necessary; string with error message if the update failed.
|
||||
*/
|
||||
enum class OTAResultStatus {
|
||||
TryAgain, // caller must deferResponse() and retry - need additional resources (JSON lock) to complete validation
|
||||
Replied, // response already sent; no action needed
|
||||
Ready, // result available; send response based on error string
|
||||
};
|
||||
std::pair<OTAResultStatus, String> getOTAResult(AsyncWebServerRequest *request);
|
||||
std::pair<bool, String> getOTAResult(AsyncWebServerRequest *request);
|
||||
|
||||
/**
|
||||
* Process a block of OTA data. This is a passthrough of an ArUploadHandlerFunction.
|
||||
|
||||
@@ -43,7 +43,7 @@ enum struct PinOwner : uint8_t {
|
||||
Relay = 0x87, // 'Rly' == Relay pin from configuration
|
||||
SPI_RAM = 0x88, // 'SpiR' == SPI RAM
|
||||
DebugOut = 0x89, // 'Dbg' == debug output always IO1
|
||||
DMX = 0x8A, // 'DMX' == DMX output, hard-coded to IO2
|
||||
DMX = 0x8A, // 'DMX' == hard-coded to IO2
|
||||
HW_I2C = 0x8B, // 'I2C' == hardware I2C pins (4&5 on ESP8266, 21&22 on ESP32)
|
||||
HW_SPI = 0x8C, // 'SPI' == hardware (V)SPI pins (13,14&15 on ESP8266, 5,18&23 on ESP32)
|
||||
DMX_INPUT = 0x8D, // 'DMX_INPUT' == DMX input via serial
|
||||
|
||||
+1
-1
@@ -212,7 +212,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage)
|
||||
char ld[4] = "LD"; ld[2] = offset+s; ld[3] = 0; //driver type (RMT=0, I2S=1)
|
||||
char hs[4] = "HS"; hs[2] = offset+s; hs[3] = 0; //hostname (for network types, custom text for others)
|
||||
if (!request->hasArg(lp)) {
|
||||
DEBUG_PRINTF_P(PSTR("# of buses: %d\n"), s);
|
||||
DEBUG_PRINTF_P(PSTR("# of buses: %d\n"), s+1);
|
||||
break;
|
||||
}
|
||||
for (int i = 0; i < 5; i++) {
|
||||
|
||||
@@ -21,10 +21,10 @@
|
||||
#include "../network/Network.h"
|
||||
#include <string.h>
|
||||
|
||||
// E1.17 ACN Packet Identifier "ASC-E1.17"
|
||||
// E1.17 ACN Packet Identifier
|
||||
const byte ESPAsyncE131::ACN_ID[12] = { 0x41, 0x53, 0x43, 0x2d, 0x45, 0x31, 0x2e, 0x31, 0x37, 0x00, 0x00, 0x00 };
|
||||
|
||||
// Art-Net Packet Identifier "Art-Net"
|
||||
// Art-Net Packet Identifier
|
||||
const byte ESPAsyncE131::ART_ID[8] = { 0x41, 0x72, 0x74, 0x2d, 0x4e, 0x65, 0x74, 0x00 };
|
||||
|
||||
// Constructor
|
||||
@@ -100,16 +100,13 @@ 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;
|
||||
const size_t pktLen = _packet.length();
|
||||
|
||||
e131_packet_t *sbuff = reinterpret_cast<e131_packet_t *>(_packet.data());
|
||||
|
||||
// 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_ARTNET;
|
||||
}
|
||||
|
||||
|
||||
//E1.31 packet identifier ("ACS-E1.17")
|
||||
if (memcmp(sbuff->acn_id, ESPAsyncE131::ACN_ID, sizeof(sbuff->acn_id)))
|
||||
protocol = P_ARTNET;
|
||||
|
||||
if (protocol == P_ARTNET) {
|
||||
if (memcmp(sbuff->art_id, ESPAsyncE131::ART_ID, sizeof(sbuff->art_id)))
|
||||
error = true; //not "Art-Net"
|
||||
@@ -132,6 +129,6 @@ void ESPAsyncE131::parsePacket(AsyncUDPPacket _packet) {
|
||||
}
|
||||
|
||||
if (!error) {
|
||||
_callback(sbuff, _packet.remoteIP(), protocol, pktLen);
|
||||
_callback(sbuff, _packet.remoteIP(), protocol);
|
||||
}
|
||||
}
|
||||
@@ -55,24 +55,19 @@ 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 // 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_QUERY 0x02
|
||||
#define DDP_FLAGS_REPLY 0x04
|
||||
#define DDP_FLAGS_STORAGE 0x08
|
||||
#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
|
||||
|
||||
// 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 DDP_ID_DISPLAY 1
|
||||
#define DDP_ID_CONFIG 250
|
||||
#define DDP_ID_STATUS 251
|
||||
|
||||
#define ARTNET_OPCODE_OPDMX 0x5000
|
||||
#define ARTNET_OPCODE_OPPOLL 0x2000
|
||||
@@ -217,7 +212,7 @@ typedef union {
|
||||
} ArtPollReply;
|
||||
|
||||
// new packet callback
|
||||
typedef void (*e131_packet_callback_function) (e131_packet_t* p, IPAddress clientIP, byte protocol, size_t packetLen);
|
||||
typedef void (*e131_packet_callback_function) (e131_packet_t* p, IPAddress clientIP, byte protocol);
|
||||
|
||||
class ESPAsyncE131 {
|
||||
private:
|
||||
|
||||
@@ -85,7 +85,6 @@ private:
|
||||
IPAddress ipMulti;
|
||||
uint32_t mac24; //bottom 24 bits of mac
|
||||
String escapedMac=""; //lowercase mac address
|
||||
String bridgeId=""; //uppercase EUI-64 bridge ID (16 hex chars)
|
||||
|
||||
//private member functions
|
||||
const char* modeString(EspalexaColorMode m)
|
||||
@@ -298,13 +297,13 @@ private:
|
||||
|
||||
snprintf_P(buf, sizeof(buf), PSTR("HTTP/1.1 200 OK\r\n"
|
||||
"EXT:\r\n"
|
||||
"CACHE-CONTROL: max-age=86400\r\n" // SSDP_INTERVAL
|
||||
"CACHE-CONTROL: max-age=100\r\n" // SSDP_INTERVAL
|
||||
"LOCATION: http://%s:80/description.xml\r\n"
|
||||
"SERVER: FreeRTOS/6.0.5, UPnP/1.0, IpBridge/1.17.0\r\n" // _modelName, _modelNumber
|
||||
"hue-bridgeid: %s\r\n"
|
||||
"ST: urn:schemas-upnp-org:device:Basic:1\r\n" // _deviceType
|
||||
"USN: uuid:2f402f80-da50-11e1-9b23-%s::urn:schemas-upnp-org:device:Basic:1\r\n" // _uuid::_deviceType
|
||||
"\r\n"),s,bridgeId.c_str(),escapedMac.c_str());
|
||||
"ST: urn:schemas-upnp-org:device:basic:1\r\n" // _deviceType
|
||||
"USN: uuid:2f402f80-da50-11e1-9b23-%s::upnp:rootdevice\r\n" // _uuid::_deviceType
|
||||
"\r\n"),s,escapedMac.c_str(),escapedMac.c_str());
|
||||
|
||||
espalexaUdp.beginPacket(espalexaUdp.remoteIP(), espalexaUdp.remotePort());
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
@@ -334,11 +333,6 @@ public:
|
||||
escapedMac.replace(":", "");
|
||||
escapedMac.toLowerCase();
|
||||
|
||||
// Compute EUI-64 bridge ID from MAC-48: insert standard "FFFE" padding between
|
||||
// the first 6 hex chars (OUI/manufacturer) and last 6 hex chars (device), then uppercase
|
||||
bridgeId = escapedMac.substring(0, 6) + "fffe" + escapedMac.substring(6);
|
||||
bridgeId.toUpperCase();
|
||||
|
||||
String macSubStr = escapedMac.substring(6, 12);
|
||||
mac24 = strtol(macSubStr.c_str(), 0, 16);
|
||||
|
||||
|
||||
@@ -804,7 +804,6 @@ 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);
|
||||
|
||||
+4
-30
@@ -317,36 +317,10 @@ uint8_t extractModeName(uint8_t mode, const char *src, char *dest, uint8_t maxLe
|
||||
} else return 0;
|
||||
}
|
||||
|
||||
if (src == JSON_palette_names) {
|
||||
if (mode > WLED_CUSTOM_PALETTE_ID_BASE) {
|
||||
// usermod palette (IDs 201-255)
|
||||
uint8_t umIdx = WLED_USERMOD_PALETTE_ID_BASE - mode;
|
||||
if (umIdx >= usermodPalettes.size()) {
|
||||
dest[0] = '\0'; // empty string if requested index is out of bounds
|
||||
return 0;
|
||||
}
|
||||
const UsermodPalette &ump = usermodPalettes[umIdx];
|
||||
char base[33];
|
||||
strncpy_P(base, ump.name, sizeof(base) - 1);
|
||||
base[sizeof(base) - 1] = '\0';
|
||||
if (ump.palName) {
|
||||
// usermod supplied a specific display name — prefix with the usermod name (e.g. "AudioReactive: Hue")
|
||||
char palName[33];
|
||||
strncpy_P(palName, ump.palName, sizeof(palName) - 1);
|
||||
palName[sizeof(palName) - 1] = '\0';
|
||||
snprintf(dest, maxLen + 1, "%s: %s", base, palName);
|
||||
} else {
|
||||
// fallback: "UMName index" (e.g. "AudioReactive 1")
|
||||
snprintf(dest, maxLen + 1, "%s %u", base, (unsigned)ump.palIndex);
|
||||
}
|
||||
return strlen(dest);
|
||||
}
|
||||
if (mode >= FIXED_PALETTE_COUNT && mode <= WLED_CUSTOM_PALETTE_ID_BASE) {
|
||||
// user custom palette (IDs FIXED_PALETTE_COUNT up to WLED_CUSTOM_PALETTE_ID_BASE=200)
|
||||
snprintf_P(dest, maxLen, PSTR("~ Custom %d ~"), WLED_CUSTOM_PALETTE_ID_BASE - mode);
|
||||
dest[maxLen] = '\0';
|
||||
return strlen(dest);
|
||||
}
|
||||
if (src == JSON_palette_names && mode > 255-customPalettes.size()) {
|
||||
snprintf_P(dest, maxLen, PSTR("~ Custom %d ~"), 255-mode);
|
||||
dest[maxLen] = '\0';
|
||||
return strlen(dest);
|
||||
}
|
||||
|
||||
unsigned qComma = 0;
|
||||
|
||||
+1
-3
@@ -369,10 +369,8 @@ void WLED::setup()
|
||||
#endif
|
||||
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
gpio_pulldown_en((gpio_num_t)hardwareRX); delay(1); // suppress noise in case RX pin is floating (at low noise energy) - see issue #3128
|
||||
// note: can not use pinMode(): it routes GPIO through the GPIO matrix and detaches UART0 RX
|
||||
pinMode(hardwareRX, INPUT_PULLDOWN); delay(1); // suppress noise in case RX pin is floating (at low noise energy) - see issue #3128
|
||||
#endif
|
||||
|
||||
#ifdef WLED_BOOTUPDELAY
|
||||
delay(WLED_BOOTUPDELAY); // delay to let voltage stabilize, helps with boot issues on some setups
|
||||
#endif
|
||||
|
||||
+5
-6
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
// version code in format yymmddb (b = daily build)
|
||||
#define VERSION 2605010
|
||||
#define VERSION 2602141
|
||||
|
||||
//uncomment this if you have a "my_config.h" file you'd like to use
|
||||
//#define WLED_USE_MY_CONFIG
|
||||
@@ -469,9 +469,9 @@ WLED_GLOBAL bool arlsForceMaxBri _INIT(false); // enable to f
|
||||
WLED_GLOBAL uint16_t DMXStartLED _INIT(0); // LED from which DMX fixtures start
|
||||
#endif
|
||||
#ifdef WLED_ENABLE_DMX_INPUT
|
||||
WLED_GLOBAL int dmxInputTransmitPin _INIT(-1);
|
||||
WLED_GLOBAL int dmxInputReceivePin _INIT(-1);
|
||||
WLED_GLOBAL int dmxInputEnablePin _INIT(-1);
|
||||
WLED_GLOBAL int dmxInputTransmitPin _INIT(0);
|
||||
WLED_GLOBAL int dmxInputReceivePin _INIT(0);
|
||||
WLED_GLOBAL int dmxInputEnablePin _INIT(0);
|
||||
WLED_GLOBAL int dmxInputPort _INIT(2);
|
||||
WLED_GLOBAL DMXInput dmxInput;
|
||||
#endif
|
||||
@@ -599,8 +599,7 @@ WLED_GLOBAL bool wasConnected _INIT(false);
|
||||
|
||||
// color
|
||||
WLED_GLOBAL byte lastRandomIndex _INIT(0); // used to save last random color so the new one is not the same
|
||||
WLED_GLOBAL std::vector<CRGBPalette16> customPalettes; // custom palettes (file-based, IDs grow downwards starting at 200)
|
||||
WLED_GLOBAL std::vector<UsermodPalette> usermodPalettes; // usermod-registered palettes (IDs 255, 254, 253...)
|
||||
WLED_GLOBAL std::vector<CRGBPalette16> customPalettes; // custom palettes
|
||||
WLED_GLOBAL uint8_t paletteBlend _INIT(0); // determines blending and wrapping of palette: 0: blend, wrap if moving (SEGMENT.speed>0); 1: blend, always wrap; 2: blend, never wrap; 3: don't blend or wrap
|
||||
|
||||
// transitions
|
||||
|
||||
@@ -506,9 +506,7 @@ void initServer()
|
||||
server.on(_update, HTTP_POST, [](AsyncWebServerRequest *request){
|
||||
if (request->_tempObject) {
|
||||
auto ota_result = getOTAResult(request);
|
||||
if (ota_result.first == OTAResultStatus::TryAgain) {
|
||||
request->deferResponse();
|
||||
} else if (ota_result.first == OTAResultStatus::Ready) {
|
||||
if (ota_result.first) {
|
||||
if (ota_result.second.length() > 0) {
|
||||
serveMessage(request, 500, F("Update failed!"), ota_result.second, 254);
|
||||
} else {
|
||||
|
||||
+9
-3
@@ -87,13 +87,19 @@ 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, len - offset);
|
||||
handleE131Packet((e131_packet_t*)&data[offset], client->remoteIP(), P_E131);
|
||||
break;
|
||||
case BINARY_PROTOCOL_ARTNET:
|
||||
handleE131Packet((e131_packet_t*)&data[offset], client->remoteIP(), P_ARTNET, len - offset);
|
||||
handleE131Packet((e131_packet_t*)&data[offset], client->remoteIP(), P_ARTNET);
|
||||
break;
|
||||
case BINARY_PROTOCOL_DDP:
|
||||
handleE131Packet((e131_packet_t*)&data[offset], client->remoteIP(), P_DDP, len - offset);
|
||||
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);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -185,18 +185,6 @@ static void appendGPIOinfo(Print& settingsScript)
|
||||
#else
|
||||
settingsScript.print(F("d.touch=[];"));
|
||||
#endif
|
||||
|
||||
// add info about ADC-capable GPIO (for analog button pin filtering)
|
||||
settingsScript.print(F("d.adc=["));
|
||||
firstPin = true;
|
||||
for (unsigned i = 0; i < WLED_NUM_PINS; i++) {
|
||||
if (PinManager::isAnalogPin(i)) {
|
||||
if (!firstPin) settingsScript.print(',');
|
||||
settingsScript.print(i);
|
||||
firstPin = false;
|
||||
}
|
||||
}
|
||||
settingsScript.print(F("];"));
|
||||
}
|
||||
|
||||
//get values for settings form in javascript
|
||||
|
||||
Reference in New Issue
Block a user