Compare commits
11 Commits
a8463fa510
...
mdev
| Author | SHA1 | Date | |
|---|---|---|---|
| ebc4498d89 | |||
| 95137a6d65 | |||
|
|
3a01c00635 | ||
|
|
e4c9fd5c62 | ||
|
|
08529a744e | ||
|
|
64529bbd84 | ||
|
|
e4c8e349da | ||
|
|
a718caf4f7 | ||
|
|
6c9922d072 | ||
|
|
231373acac | ||
|
|
eb2352774d |
@@ -3,10 +3,10 @@
|
|||||||
# CodeRabbit configuration — references existing guideline files to avoid
|
# CodeRabbit configuration — references existing guideline files to avoid
|
||||||
# duplicating conventions. See:
|
# duplicating conventions. See:
|
||||||
# .github/copilot-instructions.md — project overview & general rules
|
# .github/copilot-instructions.md — project overview & general rules
|
||||||
# .github/cpp.instructions.md — C++ coding conventions
|
# docs/cpp.instructions.md — C++ coding conventions
|
||||||
# .github/web.instructions.md — Web UI coding conventions
|
# docs/web.instructions.md — Web UI coding conventions
|
||||||
# .github/cicd.instructions.md — GitHub Actions / CI-CD conventions
|
# docs/cicd.instructions.md — GitHub Actions / CI-CD conventions
|
||||||
# .github/esp-idf.instructions.md — ESP-IDF / chip-specific coding guidelines
|
# docs/esp-idf.instructions.md — ESP-IDF / chip-specific coding guidelines
|
||||||
# (apply when code directly uses ESP-IDF APIs:
|
# (apply when code directly uses ESP-IDF APIs:
|
||||||
# esp_idf_*, I2S, RMT, ADC, GPIO, heap_caps, etc.)
|
# esp_idf_*, I2S, RMT, ADC, GPIO, heap_caps, etc.)
|
||||||
#
|
#
|
||||||
@@ -20,11 +20,11 @@ reviews:
|
|||||||
path_instructions:
|
path_instructions:
|
||||||
- path: "**/*.{cpp,h,hpp,ino}"
|
- path: "**/*.{cpp,h,hpp,ino}"
|
||||||
instructions: >
|
instructions: >
|
||||||
Follow the C++ coding conventions documented in .github/cpp.instructions.md
|
Follow the C++ coding conventions documented in docs/cpp.instructions.md
|
||||||
and the general project guidelines in .github/copilot-instructions.md.
|
and the general project guidelines in .github/copilot-instructions.md.
|
||||||
If the code under review directly uses ESP-IDF APIs (e.g. heap_caps_malloc,
|
If the code under review directly uses ESP-IDF APIs (e.g. heap_caps_malloc,
|
||||||
I2S, RMT, ADC, GPIO, esp_timer, or any esp_idf_* / soc_* symbols), also
|
I2S, RMT, ADC, GPIO, esp_timer, or any esp_idf_* / soc_* symbols), also
|
||||||
apply the guidelines in .github/esp-idf.instructions.md.
|
apply the guidelines in docs/esp-idf.instructions.md.
|
||||||
|
|
||||||
Key rules: 2-space indentation (no tabs), camelCase functions/variables,
|
Key rules: 2-space indentation (no tabs), camelCase functions/variables,
|
||||||
PascalCase classes, UPPER_CASE macros. Mark WLED-MM-specific changes with
|
PascalCase classes, UPPER_CASE macros. Mark WLED-MM-specific changes with
|
||||||
@@ -36,7 +36,7 @@ reviews:
|
|||||||
|
|
||||||
- path: "wled00/data/**"
|
- path: "wled00/data/**"
|
||||||
instructions: >
|
instructions: >
|
||||||
Follow the web UI conventions documented in .github/web.instructions.md.
|
Follow the web UI conventions documented in docs/web.instructions.md.
|
||||||
|
|
||||||
Key rules: indent HTML and JavaScript with tabs, CSS with tabs or spaces.
|
Key rules: indent HTML and JavaScript with tabs, CSS with tabs or spaces.
|
||||||
Files here are built into wled00/html_*.h by tools/cdata.js — never
|
Files here are built into wled00/html_*.h by tools/cdata.js — never
|
||||||
@@ -54,11 +54,11 @@ reviews:
|
|||||||
Each usermod lives in its own directory under usermods/ and is implemented
|
Each usermod lives in its own directory under usermods/ and is implemented
|
||||||
as a .h file that is pulled in by wled00/usermods_list.cpp (guarded by
|
as a .h file that is pulled in by wled00/usermods_list.cpp (guarded by
|
||||||
#ifdef). Usermods do not use library.json. Follow the same C++ conventions
|
#ifdef). Usermods do not use library.json. Follow the same C++ conventions
|
||||||
as the core firmware (.github/cpp.instructions.md).
|
as the core firmware (docs/cpp.instructions.md).
|
||||||
|
|
||||||
- path: ".github/workflows/*.{yml,yaml}"
|
- path: ".github/workflows/*.{yml,yaml}"
|
||||||
instructions: >
|
instructions: >
|
||||||
Follow the CI/CD conventions documented in .github/cicd.instructions.md.
|
Follow the CI/CD conventions documented in docs/cicd.instructions.md.
|
||||||
|
|
||||||
Key rules: 2-space indentation, descriptive name: on every workflow/job/step.
|
Key rules: 2-space indentation, descriptive name: on every workflow/job/step.
|
||||||
Third-party actions must be pinned to a specific version tag — branch pins
|
Third-party actions must be pinned to a specific version tag — branch pins
|
||||||
@@ -67,7 +67,7 @@ reviews:
|
|||||||
into run: steps — pass them through an env: variable to prevent script
|
into run: steps — pass them through an env: variable to prevent script
|
||||||
injection. Do not use pull_request_target unless fully justified.
|
injection. Do not use pull_request_target unless fully justified.
|
||||||
|
|
||||||
- path: ".github/*.instructions.md"
|
- path: "**/*.instructions.md"
|
||||||
instructions: |
|
instructions: |
|
||||||
This file contains both AI-facing rules and human-only reference sections.
|
This file contains both AI-facing rules and human-only reference sections.
|
||||||
Human-only sections are enclosed in `<!-- HUMAN_ONLY_START -->` /
|
Human-only sections are enclosed in `<!-- HUMAN_ONLY_START -->` /
|
||||||
|
|||||||
18
.github/agent-build.instructions.md
vendored
18
.github/agent-build.instructions.md
vendored
@@ -11,7 +11,7 @@ Use these timeout values when running builds:
|
|||||||
|
|
||||||
| Command | Typical Time | Minimum Timeout | Notes |
|
| Command | Typical Time | Minimum Timeout | Notes |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `npm run build` | ~3 s | 30 s | Web UI → `wled00/html_*.h` headers |
|
| `npm run build` | ~3 s | 30 s | Web UI → `wled00/html_*.h` `wled00/js_*.h` headers |
|
||||||
| `npm test` | ~40 s | 2 min | Validates build system |
|
| `npm test` | ~40 s | 2 min | Validates build system |
|
||||||
| `npm run dev` | continuous | — | Watch mode, auto-rebuilds on changes |
|
| `npm run dev` | continuous | — | Watch mode, auto-rebuilds on changes |
|
||||||
| `pio run -e <env>` | 15–20 min | 30 min | First build downloads toolchains; subsequent builds are faster |
|
| `pio run -e <env>` | 15–20 min | 30 min | First build downloads toolchains; subsequent builds are faster |
|
||||||
@@ -20,16 +20,21 @@ Use these timeout values when running builds:
|
|||||||
|
|
||||||
## Development Workflow
|
## Development Workflow
|
||||||
|
|
||||||
|
### Code Style Summary
|
||||||
|
- **C++** files in `wled00/` and `usermods/`: 2-space indentation (no tabs), camelCase functions/variables, PascalCase classes, UPPER_CASE macros. No C++ exceptions — use return codes and debug macros.
|
||||||
|
- **Web UI** files in `wled00/data`: indent HTML and JavaScript with tabs, CSS with tabs.
|
||||||
|
- **CI/CD workflows** in `.github/workflows`: 2-space indentation, descriptive `name:` on every workflow/job/step. Third-party actions must be pinned to a specific version tag — branch pins such as `@main` or `@master` are not allowed. SHA pinning recommended.
|
||||||
|
|
||||||
### Web UI Changes
|
### Web UI Changes
|
||||||
|
|
||||||
1. Edit files in `wled00/data/`
|
1. Edit files in `wled00/data/`
|
||||||
2. Run `npm run build` to regenerate `wled00/html_*.h` headers
|
2. Run `npm run build` to regenerate `wled00/html_*.h` `wled00/js_*.h` headers
|
||||||
3. Test with local HTTP server (see Manual Testing below)
|
3. Test with local HTTP server (see Manual Testing below)
|
||||||
4. Run `npm test` to validate
|
4. Run `npm test` to validate
|
||||||
|
|
||||||
### Firmware Changes
|
### Firmware Changes
|
||||||
|
|
||||||
1. Edit files in `wled00/` (but **never** `html_*.h` files)
|
1. Edit files in `wled00/` (but **never** `html_*.h` and `js_*.h` files)
|
||||||
2. Ensure web UI is built first: `npm run build`
|
2. Ensure web UI is built first: `npm run build`
|
||||||
3. Build firmware: `pio run -e esp32_4MB_V4_M` (set timeout ≥ 30 min)
|
3. Build firmware: `pio run -e esp32_4MB_V4_M` (set timeout ≥ 30 min)
|
||||||
4. Flash to device: `pio run -e [target] --target upload`
|
4. Flash to device: `pio run -e [target] --target upload`
|
||||||
@@ -85,8 +90,8 @@ Test these scenarios after every web UI change:
|
|||||||
### Recovery Steps
|
### Recovery Steps
|
||||||
|
|
||||||
- **Force web UI rebuild**: `npm run build -- -f`
|
- **Force web UI rebuild**: `npm run build -- -f`
|
||||||
- **Clear generated files**: `rm -f wled00/html_*.h` then `npm run build`
|
- **Clear generated files**: `rm -f wled00/html_*.h wled00/js_*.h` then `npm run build`
|
||||||
- **Clean PlatformIO cache**: `pio run --target clean`
|
- **Clean PlatformIO build artifacts**: `pio run --target clean`
|
||||||
- **Reinstall Node deps**: `rm -rf node_modules && npm ci`
|
- **Reinstall Node deps**: `rm -rf node_modules && npm ci`
|
||||||
|
|
||||||
## CI/CD Validation
|
## CI/CD Validation
|
||||||
@@ -106,7 +111,8 @@ Match this workflow in local development to catch failures before pushing.
|
|||||||
|
|
||||||
## Important Reminders
|
## Important Reminders
|
||||||
|
|
||||||
- **Never edit or commit** `wled00/html_*.h` — auto-generated from `wled00/data/`
|
- Always **commit source code**
|
||||||
|
- **Never edit or commit** `wled00/html_*.h` and `wled00/js_*.h` — auto-generated from `wled00/data/`
|
||||||
- Web UI rebuild is part of the PlatformIO firmware compilation pipeline
|
- Web UI rebuild is part of the PlatformIO firmware compilation pipeline
|
||||||
- Common firmware environments: `esp32_4MB_V4_M`, `esp32_16MB_V4_S_HUB75`, `esp32S3_8MB_PSRAM_M_qspi`, `esp32_16MB_V4_M_eth`, `esp8266_4MB_S` (deprecated), `esp32dev_compat`
|
- Common firmware environments: `esp32_4MB_V4_M`, `esp32_16MB_V4_S_HUB75`, `esp32S3_8MB_PSRAM_M_qspi`, `esp32_16MB_V4_M_eth`, `esp8266_4MB_S` (deprecated), `esp32dev_compat`
|
||||||
- List all PlatformIO targets: `pio run --list-targets`
|
- List all PlatformIO targets: `pio run --list-targets`
|
||||||
|
|||||||
33
.github/copilot-instructions.md
vendored
33
.github/copilot-instructions.md
vendored
@@ -33,29 +33,28 @@ Always reference these instructions first and fallback to search or bash command
|
|||||||
|
|
||||||
| Command | Purpose | Typical Time |
|
| Command | Purpose | Typical Time |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `npm run build` | Build web UI → generates `wled00/html_*.h` headers | ~3 s |
|
| `npm run build` | Build web UI → generates `wled00/html_*.h` and `wled00/js_*.h` headers | ~3 s |
|
||||||
| `npm test` | Run test suite | ~40 s |
|
| `npm test` | Run test suite | ~40 s |
|
||||||
| `npm run dev` | Watch mode — auto-rebuilds web UI on file changes | — |
|
| `npm run dev` | Watch mode — auto-rebuilds web UI on file changes | — |
|
||||||
| `pio run -e <env>` | Build firmware for a hardware target | 15–20 min |
|
| `pio run -e <env>` | Build firmware for a hardware target | 15–20 min |
|
||||||
|
|
||||||
<!-- HUMAN_ONLY_END -->
|
<!-- HUMAN_ONLY_END -->
|
||||||
|
|
||||||
**Always run `npm ci; npm run build` before `pio run`.** The web UI build generates `wled00/html_*.h` header files required by firmware compilation.
|
- **Always run `npm run build` before any `pio run`** (and run `npm ci` first on fresh clones or when lockfile/dependencies change).
|
||||||
**Build firmware to validate code changes**: `pio run -e esp32_4MB_V4_M` — must succeed, never skip this step.
|
- The web UI build generates required `wled00/html_*.h` and `wled00/js_*.h` headers for firmware compilation.
|
||||||
Common firmware environments: `esp32_4MB_V4_M`, `esp32_16MB_V4_S_HUB75`, `esp32S3_8MB_PSRAM_M_qspi`, `esp32_16MB_V4_M_eth`, `esp32dev_compat`, `esp8266_4MB_S` (deprecated)
|
- **Build firmware to validate code changes**: `pio run -e esp32_4MB_V4_M` — must succeed, never skip this step.
|
||||||
|
- Common firmware environments: `esp32_4MB_V4_M`, `esp32_16MB_V4_S_HUB75`, `esp32S3_8MB_PSRAM_M_qspi`, `esp32_16MB_V4_M_eth`, `esp32dev_compat`, `esp8266_4MB_S` (deprecated)
|
||||||
|
|
||||||
For detailed build timeouts, development workflows, troubleshooting, and validation steps, see [agent-build.instructions.md](agent-build.instructions.md).
|
For detailed build timeouts, development workflows, troubleshooting, and validation steps, see [agent-build.instructions.md](agent-build.instructions.md).
|
||||||
|
|
||||||
## Repository Structure
|
## Repository Structure
|
||||||
|
|
||||||
tl;dr:
|
tl;dr:
|
||||||
* Firmware source: `wled00/` (C++).
|
* Firmware source: `wled00/` (C++). Web UI source: `wled00/data/`. Build targets: `platformio.ini`.
|
||||||
* Build targets: `platformio.ini`.
|
* Auto-generated headers: `wled00/html_*.h` and `wled00/js_*.h` — **never edit or commit**.
|
||||||
* Web UI source: `wled00/data/`.
|
* ArduinoJSON + AsyncJSON: `wled00/src/dependencies/json` (included via `wled.h`). CI/CD: `.github/workflows/`.
|
||||||
* Auto-generated headers: `wled00/html_*.h` — **never edit or commit**.
|
|
||||||
* ArduinoJSON + AsyncJSON: `wled00/src/dependencies/json`
|
|
||||||
* Usermods: `usermods/` (`.h` files, included via `usermods_list.cpp`).
|
* Usermods: `usermods/` (`.h` files, included via `usermods_list.cpp`).
|
||||||
* CI/CD: `.github/workflows/`.
|
* Contributor docs: `docs/` (coding guidelines, design docs).
|
||||||
|
|
||||||
Main development trunk: `mdev` branch. Make PRs against this branch.
|
Main development trunk: `mdev` branch. Make PRs against this branch.
|
||||||
|
|
||||||
@@ -79,29 +78,33 @@ tools/ # Build tools (Node.js), partition files, and generi
|
|||||||
tools/cdata.js # Web UI → header build script
|
tools/cdata.js # Web UI → header build script
|
||||||
tools/cdata-test.js # Test suite
|
tools/cdata-test.js # Test suite
|
||||||
package.json # Node.js scripts and release ID
|
package.json # Node.js scripts and release ID
|
||||||
|
docs/ # Contributor docs: coding guidelines and design documentation
|
||||||
.github/workflows/ # CI/CD pipelines
|
.github/workflows/ # CI/CD pipelines
|
||||||
```
|
```
|
||||||
<!-- HUMAN_ONLY_END -->
|
|
||||||
|
|
||||||
|
<!-- HUMAN_ONLY_END -->
|
||||||
## General Guidelines
|
## General Guidelines
|
||||||
|
|
||||||
- **Never edit or commit** `wled00/html_*.h` — auto-generated from `wled00/data/`.
|
|
||||||
- **Repository language is English.** Suggest translations for non-English content.
|
- **Repository language is English.** Suggest translations for non-English content.
|
||||||
- **Use VS Code with PlatformIO extension** for best development experience.
|
- **Use VS Code with PlatformIO extension** for best development experience.
|
||||||
|
- **Never edit or commit** `wled00/html_*.h` and `wled00/js_*.h` — auto-generated from `wled00/data/`.
|
||||||
|
- If updating Web UI files in `wled00/data/`, **make use of common functions in `wled00/data/common.js` whenever possible**.
|
||||||
- **When unsure, say so.** Gather more information rather than guessing.
|
- **When unsure, say so.** Gather more information rather than guessing.
|
||||||
- **Acknowledge good patterns** when you see them. Summarize good practices as part of your review - 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.
|
- **Provide references** when making analyses or recommendations. Base them on the correct branch or PR.
|
||||||
- **Look for user-visible breaking changes and ripple effects**. Ask for confirmation that these were introduced intentionally.
|
- **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.
|
- **Unused / dead code must be justified or removed**. This helps to keep the codebase clean, maintainable and readable.
|
||||||
- **C++ formatting available**: `clang-format` is installed but not in CI
|
- **C++ formatting available**: `clang-format` is installed but not in CI
|
||||||
- No automated linting is configured — match existing code style in files you edit. See `cpp.instructions.md` and `web.instructions.md` for language-specific conventions, and `cicd.instructions.md` for GitHub Actions workflows.
|
- No automated linting is configured — match existing code style in files you edit.
|
||||||
|
|
||||||
|
See `docs/cpp.instructions.md`, `docs/esp-idf.instructions.md` and `docs/web.instructions.md` for language-specific conventions, and `docs/cicd.instructions.md` for GitHub Actions workflows.
|
||||||
|
|
||||||
### Attribution for AI-generated code
|
### Attribution for AI-generated code
|
||||||
Using AI-generated code can hide the source of the inspiration / knowledge / sources it used.
|
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.
|
- 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, mark it with an `// AI: below section was generated by an AI` comment (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.
|
- 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; comment-to-code ratio > 15% is expected. Do not rephrase source code, but explain the concepts/logic behind the code.
|
- 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
|
### Pull Request Expectations
|
||||||
|
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -5,8 +5,11 @@
|
|||||||
.gitignore
|
.gitignore
|
||||||
.idea
|
.idea
|
||||||
.pio
|
.pio
|
||||||
|
.piohome
|
||||||
.pioenvs
|
.pioenvs
|
||||||
.piolibdeps
|
.piolibdeps
|
||||||
|
.tools
|
||||||
|
.venv
|
||||||
.vscode
|
.vscode
|
||||||
.vscode/extensions.json
|
.vscode/extensions.json
|
||||||
|
|
||||||
|
|||||||
@@ -76,9 +76,9 @@ When in doubt, it is easiest to replicate the code style you find in the files y
|
|||||||
Our review bot (coderabbit) has learned lots of detailed guides and hints - it will suggest them automatically when you submit a PR for review.
|
Our review bot (coderabbit) has learned lots of detailed guides and hints - it will suggest them automatically when you submit a PR for review.
|
||||||
|
|
||||||
If you are curious, these are the detailed guides:
|
If you are curious, these are the detailed guides:
|
||||||
* [C++ Coding](https://github.com/MoonModules/WLED-MM/blob/mdev/.github/cpp.instructions.md)
|
* [C++ Coding](https://github.com/MoonModules/WLED-MM/blob/mdev/docs/cpp.instructions.md)
|
||||||
* [WebUi: HTML, JS, CSS](https://github.com/MoonModules/WLED-MM/blob/mdev/.github/web.instructions.md)
|
* [WebUi: HTML, JS, CSS](https://github.com/MoonModules/WLED-MM/blob/mdev/docs/web.instructions.md)
|
||||||
* [Using ESP-IDF directly](https://github.com/MoonModules/WLED-MM/blob/mdev/.github/esp-idf.instructions.md)
|
* [Using ESP-IDF directly](https://github.com/MoonModules/WLED-MM/blob/mdev/docs/esp-idf.instructions.md)
|
||||||
|
|
||||||
Below are the main rules used the WLED-MM repository.
|
Below are the main rules used the WLED-MM repository.
|
||||||
|
|
||||||
|
|||||||
@@ -71,6 +71,8 @@ schedule:
|
|||||||
|
|
||||||
## Security
|
## Security
|
||||||
|
|
||||||
|
Important: Several current workflows still violate parts of the baseline below - migration is in progress.
|
||||||
|
|
||||||
### Permissions — Least Privilege
|
### Permissions — Least Privilege
|
||||||
|
|
||||||
Declare explicit `permissions:` blocks. The default token permissions are broad; scope them to the minimum required:
|
Declare explicit `permissions:` blocks. The default token permissions are broad; scope them to the minimum required:
|
||||||
@@ -8,7 +8,10 @@ applyTo: "**/*.cpp,**/*.h,**/*.hpp,**/*.ino"
|
|||||||
> contributor reference material. Do **not** use that content as actionable review
|
> contributor reference material. Do **not** use that content as actionable review
|
||||||
> criteria — treat it as background context only.
|
> criteria — treat it as background context only.
|
||||||
|
|
||||||
|
<!-- HUMAN_ONLY_START -->
|
||||||
|
<!-- hiding this reference, to avoid cyclic "include" loops -->
|
||||||
See also: [CONTRIBUTING.md](../CONTRIBUTING.md) for general style guidelines that apply to all contributors.
|
See also: [CONTRIBUTING.md](../CONTRIBUTING.md) for general style guidelines that apply to all contributors.
|
||||||
|
<!-- HUMAN_ONLY_END -->
|
||||||
|
|
||||||
## Formatting
|
## Formatting
|
||||||
|
|
||||||
@@ -18,13 +21,13 @@ See also: [CONTRIBUTING.md](../CONTRIBUTING.md) for general style guidelines tha
|
|||||||
- Space between keyword and parenthesis: `if (...)`, `for (...)`. No space between function name and parenthesis: `doStuff(a)`
|
- Space between keyword and parenthesis: `if (...)`, `for (...)`. No space between function name and parenthesis: `doStuff(a)`
|
||||||
- No enforced line-length limit; wrap when a line exceeds your editor width
|
- No enforced line-length limit; wrap when a line exceeds your editor width
|
||||||
|
|
||||||
<!-- HUMAN_ONLY_START -->
|
|
||||||
## Naming
|
## Naming
|
||||||
|
|
||||||
- **camelCase** for functions and variables: `setValuesFromMainSeg()`, `effectCurrent`
|
- **camelCase** for functions and variables: `setValuesFromMainSeg()`, `effectCurrent`
|
||||||
- **PascalCase** for classes and structs: `PinManagerClass`, `BusConfig`
|
- **PascalCase** for classes and structs: `PinManagerClass`, `BusConfig`
|
||||||
- **UPPER_CASE** for macros and constants: `WLED_MAX_USERMODS`, `DEFAULT_CLIENT_SSID`
|
- **UPPER_CASE** for macros and constants: `WLED_MAX_USERMODS`, `DEFAULT_CLIENT_SSID`
|
||||||
|
|
||||||
|
<!-- HUMAN_ONLY_START -->
|
||||||
## Header Guards
|
## Header Guards
|
||||||
|
|
||||||
Most headers use `#ifndef` / `#define` guards. Some newer headers add `#pragma once` before the guard:
|
Most headers use `#ifndef` / `#define` guards. Some newer headers add `#pragma once` before the guard:
|
||||||
@@ -64,6 +67,7 @@ void calculateCRC(const uint8_t* data, size_t len) {
|
|||||||
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.
|
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.
|
||||||
|
|
||||||
<!-- HUMAN_ONLY_START -->
|
<!-- HUMAN_ONLY_START -->
|
||||||
|
<!-- hidden from AI for now, as it created too many "please add a description" review findings in my first tests -->
|
||||||
- **Function & feature comments:** Every non-trivial function should have a brief comment above it describing what it does. Include a note about each parameter when the names alone are not self-explanatory:
|
- **Function & feature comments:** Every non-trivial function should have a brief comment above it describing what it does. Include a note about each parameter when the names alone are not self-explanatory:
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
@@ -77,7 +81,6 @@ uint8_t gammaCorrect(uint8_t value, float gamma);
|
|||||||
```
|
```
|
||||||
<!-- HUMAN_ONLY_END -->
|
<!-- HUMAN_ONLY_END -->
|
||||||
|
|
||||||
|
|
||||||
Short accessor-style functions (getters/setters, one-liners) may skip this if their purpose is obvious from the name.
|
Short accessor-style functions (getters/setters, one-liners) may skip this if their purpose is obvious from the name.
|
||||||
|
|
||||||
## Preprocessor & Feature Flags
|
## Preprocessor & Feature Flags
|
||||||
@@ -106,12 +109,38 @@ uint8_t gammaCorrect(uint8_t value, float gamma);
|
|||||||
- **PSRAM-aware allocation**: use `d_malloc()` (prefer DRAM), `p_malloc()` (prefer PSRAM) from `util.h`
|
- **PSRAM-aware allocation**: use `d_malloc()` (prefer DRAM), `p_malloc()` (prefer PSRAM) from `util.h`
|
||||||
- **Avoid Variable Length Arrays (VLAs)**: FreeRTOS task stacks are typically 2–8 KB. A runtime-sized VLA can silently exhaust the stack. Use fixed-size arrays or heap allocation (`d_malloc` / `p_malloc`). Any VLA must be explicitly justified in source or PR.
|
- **Avoid Variable Length Arrays (VLAs)**: FreeRTOS task stacks are typically 2–8 KB. A runtime-sized VLA can silently exhaust the stack. Use fixed-size arrays or heap allocation (`d_malloc` / `p_malloc`). Any VLA must be explicitly justified in source or PR.
|
||||||
<!-- HUMAN_ONLY_START -->
|
<!-- HUMAN_ONLY_START -->
|
||||||
GCC/Clang support VLAs as an extension (they are not part of the C++ standard), so they look like a legitimate feature — but they are allocated on the stack at runtime. On ESP32/ESP8266, a VLA whose size depends on a runtime parameter (segment dimensions, pixel counts, etc.) can silently exhaust the stack and cause the program to behave in unexpected ways or crash.
|
GCC/Clang support VLAs as an extension (they are not part of the C++ standard), so they look like a legitimate feature — but they are allocated on the stack at runtime. On ESP32/ESP8266, a VLA whose size depends on a runtime parameter (segment dimensions, pixel counts, etc.) can silently exhaust the stack and cause the program to behave in unexpected ways or crash.
|
||||||
<!-- HUMAN_ONLY_END -->
|
<!-- HUMAN_ONLY_END -->
|
||||||
- **Larger buffers** (LED data, JSON documents) should use PSRAM when available and technically feasible
|
- **Larger buffers** (LED data, JSON documents) should use PSRAM when available and technically feasible
|
||||||
- **Hot-path**: some data should stay in DRAM or IRAM for performance reasons
|
- **Hot-path**: some data should stay in DRAM or IRAM for performance reasons
|
||||||
- Memory efficiency matters, but is less critical on boards with PSRAM
|
- Memory efficiency matters, but is less critical on boards with PSRAM
|
||||||
|
|
||||||
|
Heap fragmentation is a concern:
|
||||||
|
<!-- HUMAN_ONLY_START -->
|
||||||
|
- Fragmentation can lead to crashes, even when the overall amount of available heap is still good. The C++ runtime doesn't do any "garbage collection".
|
||||||
|
<!-- HUMAN_ONLY_END -->
|
||||||
|
- Avoid frequent `d_malloc` and `d_free` inside a function, especially for small sizes.
|
||||||
|
- Avoid frequent creation / destruction of objects.
|
||||||
|
- Allocate buffers early, and try to re-use them.
|
||||||
|
- Instead of incrementally appending to a `String`, reserve the expected max buffer upfront by using the `reserve()` method.
|
||||||
|
<!-- HUMAN_ONLY_START -->
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
String result;
|
||||||
|
result.reserve(65); // pre-allocate to avoid realloc fragmentation
|
||||||
|
```
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// prefer DRAM; falls back gracefully and enforces MIN_HEAP_SIZE guard
|
||||||
|
_ledsDirty = (byte*) d_malloc(getBitArrayBytes(_len));
|
||||||
|
```
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
_mode.reserve(_modeCount); // allocate memory to prevent initial fragmentation - does not increase size()
|
||||||
|
_modeData.reserve(_modeCount); // allocate memory to prevent initial fragmentation - does not increase size()
|
||||||
|
```
|
||||||
|
<!-- HUMAN_ONLY_END -->
|
||||||
|
|
||||||
## `const` and `constexpr`
|
## `const` and `constexpr`
|
||||||
Add `const` to cached locals in hot-path code (helps the compiler keep values in registers). Pass and store objects by `const&` to avoid copies in loops.
|
Add `const` to cached locals in hot-path code (helps the compiler keep values in registers). Pass and store objects by `const&` to avoid copies in loops.
|
||||||
|
|
||||||
@@ -121,12 +150,13 @@ This pattern enables optimizations and makes intent clear to reviewers.
|
|||||||
|
|
||||||
### `const` locals
|
### `const` locals
|
||||||
|
|
||||||
Adding `const` to a local variable that is only assigned once is not necessary — but it **is** required when the variable is passed to a function that takes a `const` parameter (pointer or reference). In hot-path code, `const` on cached locals helps the compiler keep values in registers:
|
* Adding `const` to a local variable that is only assigned once is optional, but *not* strictly necessary.
|
||||||
|
* In hot-path code, `const` on cached locals may help the compiler keep values in registers:
|
||||||
|
```cpp
|
||||||
|
const uint_fast16_t cols = vWidth();
|
||||||
|
const uint_fast16_t rows = vHeight();
|
||||||
|
```
|
||||||
|
|
||||||
```cpp
|
|
||||||
const uint_fast16_t cols = virtualWidth();
|
|
||||||
const uint_fast16_t rows = virtualHeight();
|
|
||||||
```
|
|
||||||
<!-- HUMAN_ONLY_END -->
|
<!-- HUMAN_ONLY_END -->
|
||||||
### `const` references to avoid copies
|
### `const` references to avoid copies
|
||||||
|
|
||||||
@@ -175,6 +205,12 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit");
|
|||||||
#error "WLED_MAX_BUSSES exceeds hard limit"
|
#error "WLED_MAX_BUSSES exceeds hard limit"
|
||||||
#endif
|
#endif
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// using static_assert() to validate enumerated types (zero cost at runtime)
|
||||||
|
static_assert(0u == static_cast<uint8_t>(PinOwner::None),
|
||||||
|
"PinOwner::None must be zero, so default array initialization works as expected");
|
||||||
|
```
|
||||||
<!-- HUMAN_ONLY_END -->
|
<!-- HUMAN_ONLY_END -->
|
||||||
|
|
||||||
Prefer `constexpr` over `#define` for typed constants (scope-safe, debuggable). Use `static_assert` instead of `#if … #error` for compile-time validation.
|
Prefer `constexpr` over `#define` for typed constants (scope-safe, debuggable). Use `static_assert` instead of `#if … #error` for compile-time validation.
|
||||||
@@ -486,22 +522,28 @@ Always pair every `esp32SemTake` with a matching `esp32SemGive`. Choose a timeou
|
|||||||
**Important**: Not every shared resource needs a mutex. Some synchronization is guaranteed by the overall control flow. For example, `volatile bool` flags like `suspendStripService`, `doInitBusses`, `loadLedmap`, and `OTAisRunning` (declared in `wled.h`) are checked sequentially in the main loop (`wled.cpp`), so they serialize access without requiring a semaphore. Use mutexes when true concurrent access from multiple FreeRTOS tasks is possible and race-conditions can lead to unexpected behaviour. Rely on control-flow ordering when operations are sequenced within the same loop iteration.
|
**Important**: Not every shared resource needs a mutex. Some synchronization is guaranteed by the overall control flow. For example, `volatile bool` flags like `suspendStripService`, `doInitBusses`, `loadLedmap`, and `OTAisRunning` (declared in `wled.h`) are checked sequentially in the main loop (`wled.cpp`), so they serialize access without requiring a semaphore. Use mutexes when true concurrent access from multiple FreeRTOS tasks is possible and race-conditions can lead to unexpected behaviour. Rely on control-flow ordering when operations are sequenced within the same loop iteration.
|
||||||
|
|
||||||
### `delay()` vs `yield()` in FreeRTOS Tasks
|
### `delay()` vs `yield()` in FreeRTOS Tasks
|
||||||
|
<!-- HUMAN_ONLY_START -->
|
||||||
|
* On ESP32, `delay(ms)` calls `vTaskDelay(ms / portTICK_PERIOD_MS)`, which **suspends only the calling task**. The FreeRTOS scheduler immediately runs all other ready tasks.
|
||||||
|
* The Arduino `loop()` function runs inside `loopTask`. Calling `delay()` there does *not* block the network stack, audio FFT, LED DMA, nor any other FreeRTOS task.
|
||||||
|
* This differs from ESP8266, where `delay()` stalled the entire system unless `yield()` was called inside.
|
||||||
|
<!-- HUMAN_ONLY_END -->
|
||||||
|
|
||||||
On ESP32, `delay(ms)` calls `vTaskDelay(ms / portTICK_PERIOD_MS)`, which **suspends only the calling task**. The FreeRTOS scheduler immediately runs all other ready tasks. This differs from ESP8266, where `delay()` stalled the entire system unless `yield()` was called inside.
|
- On ESP32, `delay()` is generally allowed, as it helps to efficiently manage CPU usage of all tasks.
|
||||||
|
- On ESP8266, only use `delay()` and `yield()` in the main `loop()` context. If not sure, protect with `if (can_yield()) ...`.
|
||||||
|
- Do *not* use `delay()` in effects (FX.cpp) or in the hot pixel path.
|
||||||
|
- `delay()` on ``busses`` level is allowed, it might be needed to achieve exact timing in LED drivers.
|
||||||
|
- **`yield()` is a no-op in WLED-MM on ESP32.** `WLEDMM_FASTPATH` redefines `yield()` to an empty macro.
|
||||||
|
```cpp
|
||||||
|
#define yield() {} // WLEDMM: yield() is completely unnecessary on ESP32
|
||||||
|
```
|
||||||
|
|
||||||
**`delay()` in `loopTask` is allowed.** The Arduino `loop()` function runs inside `loopTask`. Calling `delay()` there does not block the network stack, audio FFT, LED DMA, or any other FreeRTOS task.
|
### IDLE Watchdog and Custom Tasks on ESP32
|
||||||
|
|
||||||
**`yield()` is a no-op in WLED-MM on ESP32.** `WLEDMM_FASTPATH` redefines `yield()` to an empty macro:
|
- **Do NOT use `yield()` to pace ESP32 tasks or assume it feeds any watchdog**.
|
||||||
|
|
||||||
```cpp
|
- Even in stock arduino-esp32, `yield()` calls `vTaskDelay(0)`, which only switches to tasks at equal or higher priority — the IDLE task (priority 0) is never reached.
|
||||||
#define yield() {} // WLEDMM: yield() is completely unnecessary on ESP32
|
|
||||||
```
|
|
||||||
|
|
||||||
Even in stock arduino-esp32, `yield()` calls `vTaskDelay(0)`, which only switches to tasks at equal or higher priority — the IDLE task (priority 0) is never reached.
|
|
||||||
**Do not use `yield()` to pace ESP32 tasks or assume it feeds any watchdog**.
|
|
||||||
|
|
||||||
**Custom `xTaskCreate()` tasks must call `delay(1)` in their loop, not `yield()`.** Without a real blocking call, the IDLE task is starved. The IDLE watchdog panic is the first visible symptom — but the damage starts earlier: deleted task memory leaks, software timers stop firing, light sleep is disabled, and Wi-Fi/BT idle hooks don't run. See `esp-idf.instructions.md` for a full explanation of what IDLE does. Structure custom tasks like this:
|
|
||||||
|
|
||||||
|
- **Custom `xTaskCreate()` tasks must call `delay(1)` in their loop, not `yield()`.** Without a real blocking call, the IDLE task is starved. The IDLE watchdog panic is the first visible symptom — but the damage starts earlier: deleted task memory leaks, software timers stop firing, light sleep is disabled, and Wi-Fi/BT idle hooks don't run. See `esp-idf.instructions.md` for a full explanation of what IDLE does. Structure custom tasks like this:
|
||||||
```cpp
|
```cpp
|
||||||
// WRONG — IDLE task is never scheduled; yield() does not feed the idle task watchdog.
|
// WRONG — IDLE task is never scheduled; yield() does not feed the idle task watchdog.
|
||||||
void myTask(void*) {
|
void myTask(void*) {
|
||||||
@@ -520,9 +562,8 @@ void myTask(void*) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Prefer blocking FreeRTOS primitives (`xQueueReceive`, `ulTaskNotifyTake`, `vTaskDelayUntil`) over `delay(1)` polling where precise timing or event-driven behaviour is needed.
|
- Prefer blocking FreeRTOS primitives (`xQueueReceive`, `ulTaskNotifyTake`, `vTaskDelayUntil`) over `delay(1)` polling where precise timing or event-driven behaviour is needed.
|
||||||
|
- **Watchdog note.** WLED-MM disables the Task Watchdog by default (`WLED_WATCHDOG_TIMEOUT 0` in `wled.h`). When enabled, `esp_task_wdt_reset()` is called at the end of each `loop()` iteration. Long blocking operations inside `loop()` — such as OTA downloads or slow file I/O — must call `esp_task_wdt_reset()` periodically, or be restructured so the main loop is not blocked for longer than the configured timeout.
|
||||||
**Watchdog note.** WLED-MM disables the Task Watchdog by default (`WLED_WATCHDOG_TIMEOUT 0` in `wled.h`). When enabled, `esp_task_wdt_reset()` is called at the end of each `loop()` iteration. Long blocking operations inside `loop()` — such as OTA downloads or slow file I/O — must call `esp_task_wdt_reset()` periodically, or be restructured so the main loop is not blocked for longer than the configured timeout.
|
|
||||||
|
|
||||||
## General
|
## General
|
||||||
|
|
||||||
@@ -530,7 +571,8 @@ Prefer blocking FreeRTOS primitives (`xQueueReceive`, `ulTaskNotifyTake`, `vTask
|
|||||||
- If possible, use `static` for local (C-style) variables and functions (keeps the global namespace clean)
|
- If possible, use `static` for local (C-style) variables and functions (keeps the global namespace clean)
|
||||||
- Avoid unexplained "magic numbers". Prefer named constants (`constexpr`) or C-style `#define` constants for repeated numbers that have the same meaning
|
- Avoid unexplained "magic numbers". Prefer named constants (`constexpr`) or C-style `#define` constants for repeated numbers that have the same meaning
|
||||||
- Include `"wled.h"` as the primary project header where needed
|
- Include `"wled.h"` as the primary project header where needed
|
||||||
- **Float-to-unsigned conversion is undefined behavior when the value is out of range.** Converting a negative `float` directly to an unsigned integer type (`uint8_t`, `uint16_t`, …) is UB per the C++ standard — the Xtensa (ESP32) toolchain may silently wrap, but RISC-V (ESP32-C3/C6) can produce different results due to clamping. Cast through a signed integer first:
|
|
||||||
|
- **Float-to-unsigned conversion is undefined behavior when the value is out of range.** Converting a negative `float` directly to an unsigned integer type (`uint8_t`, `uint16_t`, …) is UB per the C++ standard — the Xtensa (ESP32) toolchain may silently wrap, but RISC-V (ESP32-C3/C5/C6/P4) can produce different results due to clamping. Cast through a signed integer first:
|
||||||
```cpp
|
```cpp
|
||||||
// Undefined behavior — avoid:
|
// Undefined behavior — avoid:
|
||||||
uint8_t angle = 40.74f * atan2f(dy, dx); // negative float → uint8_t is UB
|
uint8_t angle = 40.74f * atan2f(dy, dx); // negative float → uint8_t is UB
|
||||||
@@ -499,6 +499,9 @@ The ESP32 has an audio PLL for precise sample rates. Rules:
|
|||||||
|
|
||||||
- Not supported on ESP32-C3 (`SOC_I2S_SUPPORTS_PDM_RX` not defined).
|
- 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.
|
- 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-MM.
|
- No clock pin (`I2S_CKPIN = -1`) triggers PDM mode in WLED-MM.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -579,13 +582,21 @@ if (!pinManager.allocatePin(myPin, true, PinOwner::UM_MyUsermod)) {
|
|||||||
|
|
||||||
### Microsecond timing
|
### Microsecond timing
|
||||||
|
|
||||||
For high-resolution timing, prefer `esp_timer_get_time()` (microsecond resolution, 64-bit) over `millis()` or `micros()`:
|
For high-resolution timing, prefer `esp_timer_get_time()` (microsecond resolution, 64-bit) over `millis()` or `micros()`.
|
||||||
|
<!-- HUMAN_ONLY_START -->
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
#include <esp_timer.h>
|
#include <esp_timer.h>
|
||||||
int64_t now_us = esp_timer_get_time(); // monotonic, not affected by NTP
|
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 -->
|
<!-- HUMAN_ONLY_START -->
|
||||||
### Periodic timers
|
### Periodic timers
|
||||||
|
|
||||||
@@ -606,6 +617,27 @@ esp_timer_start_periodic(timer, 1000); // 1 ms period
|
|||||||
|
|
||||||
Always prefer `ESP_TIMER_TASK` dispatch over `ESP_TIMER_ISR` unless you need ISR-level latency — ISR callbacks have severe restrictions (no logging, no heap allocation, no FreeRTOS API calls).
|
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
|
## ADC Best Practices
|
||||||
@@ -672,14 +704,15 @@ RMT drives NeoPixel LED output (via NeoPixelBus) and IR receiver input. Both use
|
|||||||
- New chips (C6, P4) have different RMT channel counts — use `SOC_RMT_TX_CANDIDATES_PER_GROUP` to check availability.
|
- 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.
|
- 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)
|
## Espressif Best Practices (from official examples)
|
||||||
|
|
||||||
### Error handling
|
### Error handling
|
||||||
|
|
||||||
Always check `esp_err_t` return values. Use `ESP_ERROR_CHECK()` in initialization code, but handle errors gracefully in runtime code:
|
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
|
```cpp
|
||||||
// Initialization — crash early on failure
|
// Initialization — crash early on failure
|
||||||
ESP_ERROR_CHECK(i2s_driver_install(I2S_NUM_0, &config, 0, nullptr));
|
ESP_ERROR_CHECK(i2s_driver_install(I2S_NUM_0, &config, 0, nullptr));
|
||||||
@@ -693,6 +726,17 @@ if (err != ESP_OK) {
|
|||||||
```
|
```
|
||||||
<!-- HUMAN_ONLY_END -->
|
<!-- 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
|
### Logging
|
||||||
|
|
||||||
WLED-MM uses its own logging macros — **not** `ESP_LOGx()`. For application-level code, always use the WLED-MM macros defined in `wled.h`:
|
WLED-MM uses its own logging macros — **not** `ESP_LOGx()`. For application-level code, always use the WLED-MM macros defined in `wled.h`:
|
||||||
@@ -706,13 +750,14 @@ All of these wrap `Serial` output through the `DEBUGOUT` / `DEBUGOUTLN` / `DEBUG
|
|||||||
|
|
||||||
**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:
|
**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
|
```cpp
|
||||||
static const char* TAG = "my_module";
|
static const char* TAG = "my_module";
|
||||||
ESP_LOGI(TAG, "Initialized with %d buffers", count);
|
ESP_LOGI(TAG, "Initialized with %d buffers", count);
|
||||||
ESP_LOGW(TAG, "PSRAM not available, falling back to DRAM");
|
ESP_LOGW(TAG, "PSRAM not available, falling back to DRAM");
|
||||||
ESP_LOGE(TAG, "Failed to allocate %u bytes", size);
|
ESP_LOGE(TAG, "Failed to allocate %u bytes", size);
|
||||||
```
|
```
|
||||||
|
<!-- HUMAN_ONLY_END -->
|
||||||
### Task creation and pinning
|
### Task creation and pinning
|
||||||
|
|
||||||
<!-- HUMAN_ONLY_START -->
|
<!-- HUMAN_ONLY_START -->
|
||||||
64
docs/rfp-esp32s3-wroom1-n16r8-3x106.md
Normal file
64
docs/rfp-esp32s3-wroom1-n16r8-3x106.md
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# RFP ESP32-S3 WROOM-1 N16R8 (3 x 106)
|
||||||
|
|
||||||
|
This repository includes a tracked PlatformIO target for the RFP ESP32-S3 WROOM-1 N16R8 nodes with three LED outputs and 106 pixels per output.
|
||||||
|
|
||||||
|
Build target:
|
||||||
|
|
||||||
|
- `rfp_esp32s3_wroom1_n16r8_3x106`
|
||||||
|
|
||||||
|
Default output pins:
|
||||||
|
|
||||||
|
- Output 1: `GPIO4`
|
||||||
|
- Output 2: `GPIO5`
|
||||||
|
- Output 3: `GPIO6`
|
||||||
|
|
||||||
|
Pins intentionally avoided:
|
||||||
|
|
||||||
|
- `GPIO0`, `GPIO3`, `GPIO45`, `GPIO46` for boot / strapping
|
||||||
|
- `GPIO19`, `GPIO20` for USB
|
||||||
|
- `GPIO33` to `GPIO37` because they are reserved by octal PSRAM / flash on `N16R8`
|
||||||
|
- `GPIO48` because it is used as the onboard status pixel
|
||||||
|
|
||||||
|
Build:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -m platformio run -e rfp_esp32s3_wroom1_n16r8_3x106
|
||||||
|
```
|
||||||
|
|
||||||
|
Build and upload:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\tools\flash_rfp_s3.ps1 -ComPort COM7
|
||||||
|
```
|
||||||
|
|
||||||
|
Build only with the helper script:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\tools\flash_rfp_s3.ps1 -BuildOnly
|
||||||
|
```
|
||||||
|
|
||||||
|
Local Wi-Fi defaults:
|
||||||
|
|
||||||
|
- Keep SSID and password in the ignored file `wled00/my_config.h`.
|
||||||
|
- If the file does not exist yet, create it with your local values:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#define CLIENT_SSID "your-ssid"
|
||||||
|
#define CLIENT_PASS "your-password"
|
||||||
|
```
|
||||||
|
|
||||||
|
Important:
|
||||||
|
|
||||||
|
- The `3 x 106` bus layout is used as the default when the device has no saved `cfg.json`.
|
||||||
|
- If a board already has a saved WLED config, do a factory reset or erase settings once so the default bus layout is recreated.
|
||||||
|
|
||||||
|
Onboard status pixel on `GPIO48`:
|
||||||
|
|
||||||
|
- `green blinking`: DDP realtime active
|
||||||
|
- `green solid`: network connected
|
||||||
|
- `blue blinking`: AP / setup mode active
|
||||||
|
- `red fast blinking`: Wi-Fi configured but currently disconnected
|
||||||
|
- `amber fast blinking`: network connected, MQTT configured, but MQTT not connected
|
||||||
|
- `off`: idle / no status to show
|
||||||
503
docs/rfp-node-flashing.md
Normal file
503
docs/rfp-node-flashing.md
Normal file
@@ -0,0 +1,503 @@
|
|||||||
|
# RFP Infinity Flashing
|
||||||
|
|
||||||
|
This document covers flashing for both the Infinity master and the ESP32-S3 render nodes.
|
||||||
|
|
||||||
|
## Targets
|
||||||
|
|
||||||
|
- Master target: `rfp_esp32s3_wroom1_n16r8_master`
|
||||||
|
- Conservative master cold-boot target: `rfp_esp32s3_wroom1_n16r8_master_coldboot`
|
||||||
|
- Standard node target: `rfp_esp32s3_wroom1_n16r8_3x106`
|
||||||
|
- Conservative cold-boot test target: `rfp_esp32s3_wroom1_n16r8_3x106_coldboot`
|
||||||
|
|
||||||
|
Use a cold-boot target when a board only starts reliably after pressing `RESET`
|
||||||
|
following a long power loss.
|
||||||
|
|
||||||
|
## Clean-Flash Warning
|
||||||
|
|
||||||
|
`erase_flash` removes the complete flash contents, including WLED's saved
|
||||||
|
`cfg.json`, Wi-Fi credentials, static IP settings, presets, and filesystem data.
|
||||||
|
|
||||||
|
The RFP master and node targets now compile the show Wi-Fi as firmware defaults:
|
||||||
|
|
||||||
|
- SSID: `RFPLicht`
|
||||||
|
- Password: configured in the RFP build flags
|
||||||
|
|
||||||
|
After a full erase the board can therefore join Wi-Fi again, but any runtime-only
|
||||||
|
settings that were stored through the WLED UI must be re-applied unless they are
|
||||||
|
also encoded as firmware defaults.
|
||||||
|
|
||||||
|
## Roles
|
||||||
|
|
||||||
|
- Master:
|
||||||
|
- Usually `192.168.178.10`
|
||||||
|
- Runs the `/infinity` web UI
|
||||||
|
- Accepts DMX and web commands
|
||||||
|
- Sends Infinity Sync packets to the nodes
|
||||||
|
- Keeps one dummy WLED pixel on `GPIO21` so the regular WLED UI remains
|
||||||
|
valid
|
||||||
|
- Keeps the real WLED status pixel exclusively on `GPIO48`
|
||||||
|
- Repairs old master `cfg.json` LED-bus entries so `GPIO48` is not reused as
|
||||||
|
a normal LED output
|
||||||
|
- Nodes:
|
||||||
|
- Usually `192.168.178.11` to `192.168.178.16`
|
||||||
|
- Render the LED output locally
|
||||||
|
- Receive Infinity Sync from the master
|
||||||
|
|
||||||
|
The flash procedure is similar for both roles, but the PlatformIO target and `firmware.bin` are different.
|
||||||
|
|
||||||
|
## WLED Backup Mode
|
||||||
|
|
||||||
|
The regular WLED UI is intentionally kept available as a fallback.
|
||||||
|
|
||||||
|
Important behavior:
|
||||||
|
|
||||||
|
- The master uses the WLED UI only for a dummy backup pixel on `GPIO21`; the
|
||||||
|
actual onboard status pixel remains WLED's normal status pixel on `GPIO48`.
|
||||||
|
- The master does not render the show LEDs directly.
|
||||||
|
- The nodes can still be controlled through their regular WLED UI.
|
||||||
|
- If Infinity Sync is enabled, the master sends scene state about every `100 ms`.
|
||||||
|
- While those packets arrive, the node UI may appear to ignore changes because
|
||||||
|
the next Infinity packet overwrites the local WLED state.
|
||||||
|
|
||||||
|
Use regular WLED control as backup in one of these ways:
|
||||||
|
|
||||||
|
1. Preferred: open `/infinity` on the master and use the mode button in the
|
||||||
|
top bar:
|
||||||
|
- `Show Mode: ON` means Infinity Sync is active.
|
||||||
|
- `WLED Backup: ON` means Infinity Sync is stopped and regular WLED control
|
||||||
|
can be used.
|
||||||
|
|
||||||
|
2. Or stop Infinity on the master by API:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://192.168.178.10/json/infinity \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{"enabled":false}'
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Or disable Infinity on one node for local testing:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://192.168.178.11/json/infinity \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{"enabled":false}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Re-enable later:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://192.168.178.10/json/infinity \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{"enabled":true}'
|
||||||
|
```
|
||||||
|
|
||||||
|
For hotspot testing, replace the IPs with the current addresses, for example
|
||||||
|
`10.42.0.213`.
|
||||||
|
|
||||||
|
## Build: Master
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/jan/Documents/RFP/WLED-MM/repo
|
||||||
|
|
||||||
|
PATH=/home/jan/Documents/RFP/Finanz_App/node/current/bin:$PATH \
|
||||||
|
NPM_CONFIG_CACHE=$PWD/.npm-cache \
|
||||||
|
PLATFORMIO_CORE_DIR=$PWD/.piohome \
|
||||||
|
PLATFORMIO_PACKAGES_DIR=$PWD/.piohome/packages \
|
||||||
|
PLATFORMIO_PLATFORMS_DIR=$PWD/.piohome/platforms \
|
||||||
|
PLATFORMIO_CACHE_DIR=$PWD/.piohome/.cache \
|
||||||
|
PLATFORMIO_BUILD_CACHE_DIR=$PWD/.piohome/buildcache \
|
||||||
|
.venv/bin/python -m platformio run -e rfp_esp32s3_wroom1_n16r8_master
|
||||||
|
```
|
||||||
|
|
||||||
|
Master firmware output:
|
||||||
|
|
||||||
|
```text
|
||||||
|
.pio/build/rfp_esp32s3_wroom1_n16r8_master/firmware.bin
|
||||||
|
```
|
||||||
|
|
||||||
|
Conservative master cold-boot build:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/jan/Documents/RFP/WLED-MM/repo
|
||||||
|
|
||||||
|
PATH=/home/jan/Documents/RFP/Finanz_App/node/current/bin:$PATH \
|
||||||
|
NPM_CONFIG_CACHE=$PWD/.npm-cache \
|
||||||
|
PLATFORMIO_CORE_DIR=$PWD/.piohome \
|
||||||
|
PLATFORMIO_PACKAGES_DIR=$PWD/.piohome/packages \
|
||||||
|
PLATFORMIO_PLATFORMS_DIR=$PWD/.piohome/platforms \
|
||||||
|
PLATFORMIO_CACHE_DIR=$PWD/.piohome/.cache \
|
||||||
|
PLATFORMIO_BUILD_CACHE_DIR=$PWD/.piohome/buildcache \
|
||||||
|
.venv/bin/python -m platformio run -e rfp_esp32s3_wroom1_n16r8_master_coldboot
|
||||||
|
```
|
||||||
|
|
||||||
|
Master cold-boot firmware output:
|
||||||
|
|
||||||
|
```text
|
||||||
|
.pio/build/rfp_esp32s3_wroom1_n16r8_master_coldboot/firmware.bin
|
||||||
|
```
|
||||||
|
|
||||||
|
## Build: Nodes
|
||||||
|
|
||||||
|
Standard build:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/jan/Documents/RFP/WLED-MM/repo
|
||||||
|
|
||||||
|
PATH=/home/jan/Documents/RFP/Finanz_App/node/current/bin:$PATH \
|
||||||
|
NPM_CONFIG_CACHE=$PWD/.npm-cache \
|
||||||
|
PLATFORMIO_CORE_DIR=$PWD/.piohome \
|
||||||
|
PLATFORMIO_PACKAGES_DIR=$PWD/.piohome/packages \
|
||||||
|
PLATFORMIO_PLATFORMS_DIR=$PWD/.piohome/platforms \
|
||||||
|
PLATFORMIO_CACHE_DIR=$PWD/.piohome/.cache \
|
||||||
|
PLATFORMIO_BUILD_CACHE_DIR=$PWD/.piohome/buildcache \
|
||||||
|
.venv/bin/python -m platformio run -e rfp_esp32s3_wroom1_n16r8_3x106
|
||||||
|
```
|
||||||
|
|
||||||
|
Cold-boot test build:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/jan/Documents/RFP/WLED-MM/repo
|
||||||
|
|
||||||
|
PATH=/home/jan/Documents/RFP/Finanz_App/node/current/bin:$PATH \
|
||||||
|
NPM_CONFIG_CACHE=$PWD/.npm-cache \
|
||||||
|
PLATFORMIO_CORE_DIR=$PWD/.piohome \
|
||||||
|
PLATFORMIO_PACKAGES_DIR=$PWD/.piohome/packages \
|
||||||
|
PLATFORMIO_PLATFORMS_DIR=$PWD/.piohome/platforms \
|
||||||
|
PLATFORMIO_CACHE_DIR=$PWD/.piohome/.cache \
|
||||||
|
PLATFORMIO_BUILD_CACHE_DIR=$PWD/.piohome/buildcache \
|
||||||
|
.venv/bin/python -m platformio run -e rfp_esp32s3_wroom1_n16r8_3x106_coldboot
|
||||||
|
```
|
||||||
|
|
||||||
|
Node firmware outputs:
|
||||||
|
|
||||||
|
```text
|
||||||
|
.pio/build/rfp_esp32s3_wroom1_n16r8_3x106/firmware.bin
|
||||||
|
.pio/build/rfp_esp32s3_wroom1_n16r8_3x106_coldboot/firmware.bin
|
||||||
|
```
|
||||||
|
|
||||||
|
## WLAN Flash: Master
|
||||||
|
|
||||||
|
Flash the master by OTA:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/jan/Documents/RFP/WLED-MM/repo
|
||||||
|
|
||||||
|
.venv/bin/python tools/rfp_network_flash.py flash \
|
||||||
|
--targets 192.168.178.10 \
|
||||||
|
--firmware .pio/build/rfp_esp32s3_wroom1_n16r8_master/firmware.bin
|
||||||
|
```
|
||||||
|
|
||||||
|
## WLAN Flash: Single Node
|
||||||
|
|
||||||
|
Standard firmware to one node:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/jan/Documents/RFP/WLED-MM/repo
|
||||||
|
|
||||||
|
.venv/bin/python tools/rfp_network_flash.py flash \
|
||||||
|
--targets 192.168.178.11 \
|
||||||
|
--firmware .pio/build/rfp_esp32s3_wroom1_n16r8_3x106/firmware.bin
|
||||||
|
```
|
||||||
|
|
||||||
|
## WLAN Flash: Group
|
||||||
|
|
||||||
|
Flash all six nodes in order:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/jan/Documents/RFP/WLED-MM/repo
|
||||||
|
|
||||||
|
.venv/bin/python tools/rfp_network_flash.py flash \
|
||||||
|
--targets 192.168.178.11,192.168.178.12,192.168.178.13,192.168.178.14,192.168.178.15,192.168.178.16 \
|
||||||
|
--start-from 192.168.178.11 \
|
||||||
|
--firmware .pio/build/rfp_esp32s3_wroom1_n16r8_3x106/firmware.bin
|
||||||
|
```
|
||||||
|
|
||||||
|
Resume a failed OTA run from a specific node:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/jan/Documents/RFP/WLED-MM/repo
|
||||||
|
|
||||||
|
.venv/bin/python tools/rfp_network_flash.py flash \
|
||||||
|
--targets 192.168.178.11,192.168.178.12,192.168.178.13,192.168.178.14,192.168.178.15,192.168.178.16 \
|
||||||
|
--start-from 192.168.178.14 \
|
||||||
|
--firmware .pio/build/rfp_esp32s3_wroom1_n16r8_3x106/firmware.bin
|
||||||
|
```
|
||||||
|
|
||||||
|
## WLAN Flash: Full Installation
|
||||||
|
|
||||||
|
Build standard OTA firmware locally, flash all six nodes sequentially, then flash
|
||||||
|
the master last:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/jan/Documents/RFP/WLED-MM/repo
|
||||||
|
|
||||||
|
.venv/bin/python tools/rfp_update_all_ota.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Dry-run without building or flashing:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/jan/Documents/RFP/WLED-MM/repo
|
||||||
|
|
||||||
|
.venv/bin/python tools/rfp_update_all_ota.py --dry-run --no-build
|
||||||
|
```
|
||||||
|
|
||||||
|
Resume after an interrupted run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/jan/Documents/RFP/WLED-MM/repo
|
||||||
|
|
||||||
|
.venv/bin/python tools/rfp_update_all_ota.py --start-from 192.168.178.14
|
||||||
|
```
|
||||||
|
|
||||||
|
Useful variants:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
.venv/bin/python tools/rfp_update_all_ota.py --nodes-only
|
||||||
|
.venv/bin/python tools/rfp_update_all_ota.py --master-only
|
||||||
|
.venv/bin/python tools/rfp_update_all_ota.py --no-build
|
||||||
|
.venv/bin/python tools/rfp_update_all_ota.py --subnet 192.168.178.0/24
|
||||||
|
```
|
||||||
|
|
||||||
|
The full-update helper is only for standard OTA builds:
|
||||||
|
|
||||||
|
- Nodes: `rfp_esp32s3_wroom1_n16r8_3x106`
|
||||||
|
- Master: `rfp_esp32s3_wroom1_n16r8_master`
|
||||||
|
|
||||||
|
Cold-boot targets remain USB clean-flash targets because OTA does not rewrite
|
||||||
|
the bootloader/flash-mode layout.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
- OTA only works when the laptop and nodes are already in the same IP network.
|
||||||
|
- The OTA helper flashes sequentially, verifies reboot, and then continues to the next node.
|
||||||
|
- The cold-boot test target should be flashed by USB, not by OTA.
|
||||||
|
- Reason:
|
||||||
|
- it changes flash/boot related build settings (`flash_mode`, `memory_type`)
|
||||||
|
- it also uses a different release name for validation
|
||||||
|
- OTA only updates the application image, not the full USB-style flash layout
|
||||||
|
- If you try the cold-boot target by OTA, the usual symptom is exactly this:
|
||||||
|
- upload looks "uncertain"
|
||||||
|
- device stays reachable
|
||||||
|
- uptime keeps increasing
|
||||||
|
- reboot cannot be proven
|
||||||
|
- Reboot verification is now strict:
|
||||||
|
- it records firmware version and uptime before upload
|
||||||
|
- it waits for the node to disappear from the network
|
||||||
|
- it waits for the node to come back
|
||||||
|
- if no offline transition is seen, it requires a clear uptime reset before declaring success
|
||||||
|
- This avoids false positives where a node stays reachable and the script would previously print `OK` too early.
|
||||||
|
|
||||||
|
## USB Flash: Master
|
||||||
|
|
||||||
|
Recommended clean-flash helper:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/jan/Documents/RFP/WLED-MM/repo
|
||||||
|
./flash_master.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Check the serial port:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ls /dev/ttyACM* /dev/ttyUSB* 2>/dev/null
|
||||||
|
```
|
||||||
|
|
||||||
|
Flash the master via USB:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/jan/Documents/RFP/WLED-MM/repo
|
||||||
|
|
||||||
|
sudo .venv/bin/python .piohome/packages/tool-esptoolpy/esptool.py \
|
||||||
|
--chip esp32s3 \
|
||||||
|
--port /dev/ttyACM0 \
|
||||||
|
--baud 460800 \
|
||||||
|
--before no_reset \
|
||||||
|
--after hard_reset \
|
||||||
|
write_flash -z \
|
||||||
|
--flash_mode qio \
|
||||||
|
--flash_freq 80m \
|
||||||
|
--flash_size 16MB \
|
||||||
|
0x0 .pio/build/rfp_esp32s3_wroom1_n16r8_master/bootloader.bin \
|
||||||
|
0x8000 .pio/build/rfp_esp32s3_wroom1_n16r8_master/partitions.bin \
|
||||||
|
0xe000 .piohome/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin \
|
||||||
|
0x10000 .pio/build/rfp_esp32s3_wroom1_n16r8_master/firmware.bin
|
||||||
|
```
|
||||||
|
|
||||||
|
Conservative master cold-boot firmware:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/jan/Documents/RFP/WLED-MM/repo
|
||||||
|
|
||||||
|
sudo .venv/bin/python .piohome/packages/tool-esptoolpy/esptool.py \
|
||||||
|
--chip esp32s3 \
|
||||||
|
--port /dev/ttyACM0 \
|
||||||
|
--baud 460800 \
|
||||||
|
--before no_reset \
|
||||||
|
--after hard_reset \
|
||||||
|
write_flash -z \
|
||||||
|
--flash_mode qio \
|
||||||
|
--flash_freq 80m \
|
||||||
|
--flash_size 16MB \
|
||||||
|
0x0 .pio/build/rfp_esp32s3_wroom1_n16r8_master_coldboot/bootloader.bin \
|
||||||
|
0x8000 .pio/build/rfp_esp32s3_wroom1_n16r8_master_coldboot/partitions.bin \
|
||||||
|
0xe000 .piohome/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin \
|
||||||
|
0x10000 .pio/build/rfp_esp32s3_wroom1_n16r8_master_coldboot/firmware.bin
|
||||||
|
```
|
||||||
|
|
||||||
|
Use USB for the cold-boot master target. OTA is not sufficient for this test
|
||||||
|
because the fix changes bootloader/flash-mode related build settings.
|
||||||
|
|
||||||
|
## USB Flash: Single Node
|
||||||
|
|
||||||
|
Recommended clean-flash helper:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/jan/Documents/RFP/WLED-MM/repo
|
||||||
|
./flash_node.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Check the serial port:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ls /dev/ttyACM* /dev/ttyUSB* 2>/dev/null
|
||||||
|
```
|
||||||
|
|
||||||
|
Standard firmware:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/jan/Documents/RFP/WLED-MM/repo
|
||||||
|
|
||||||
|
sudo .venv/bin/python .piohome/packages/tool-esptoolpy/esptool.py \
|
||||||
|
--chip esp32s3 \
|
||||||
|
--port /dev/ttyACM0 \
|
||||||
|
--baud 460800 \
|
||||||
|
--before no_reset \
|
||||||
|
--after hard_reset \
|
||||||
|
write_flash -z \
|
||||||
|
--flash_mode qio \
|
||||||
|
--flash_freq 80m \
|
||||||
|
--flash_size 16MB \
|
||||||
|
0x0 .pio/build/rfp_esp32s3_wroom1_n16r8_3x106/bootloader.bin \
|
||||||
|
0x8000 .pio/build/rfp_esp32s3_wroom1_n16r8_3x106/partitions.bin \
|
||||||
|
0xe000 .piohome/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin \
|
||||||
|
0x10000 .pio/build/rfp_esp32s3_wroom1_n16r8_3x106/firmware.bin
|
||||||
|
```
|
||||||
|
|
||||||
|
Cold-boot test firmware:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/jan/Documents/RFP/WLED-MM/repo
|
||||||
|
|
||||||
|
sudo .venv/bin/python .piohome/packages/tool-esptoolpy/esptool.py \
|
||||||
|
--chip esp32s3 \
|
||||||
|
--port /dev/ttyACM0 \
|
||||||
|
--baud 460800 \
|
||||||
|
--before no_reset \
|
||||||
|
--after hard_reset \
|
||||||
|
write_flash -z \
|
||||||
|
--flash_mode dio \
|
||||||
|
--flash_freq 80m \
|
||||||
|
--flash_size 16MB \
|
||||||
|
0x0 .pio/build/rfp_esp32s3_wroom1_n16r8_3x106_coldboot/bootloader.bin \
|
||||||
|
0x8000 .pio/build/rfp_esp32s3_wroom1_n16r8_3x106_coldboot/partitions.bin \
|
||||||
|
0xe000 .piohome/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin \
|
||||||
|
0x10000 .pio/build/rfp_esp32s3_wroom1_n16r8_3x106_coldboot/firmware.bin
|
||||||
|
```
|
||||||
|
|
||||||
|
If upload does not start immediately:
|
||||||
|
|
||||||
|
1. Hold `BOOT`
|
||||||
|
2. Tap `RESET`
|
||||||
|
3. Release `BOOT`
|
||||||
|
|
||||||
|
Use USB for the first flash of `rfp_esp32s3_wroom1_n16r8_3x106_coldboot`.
|
||||||
|
After that, if you go back to the normal node firmware, OTA is fine again with the standard node target.
|
||||||
|
|
||||||
|
## USB Flash: Group
|
||||||
|
|
||||||
|
USB flashing always happens physically one board after another unless several boards are connected at the same time.
|
||||||
|
|
||||||
|
Recommended workflow:
|
||||||
|
|
||||||
|
1. Build the desired target once.
|
||||||
|
2. Plug in node 1 and flash it.
|
||||||
|
3. Unplug node 1, plug in node 2, repeat.
|
||||||
|
4. Continue until node 6 is done.
|
||||||
|
|
||||||
|
If the same serial path is reused each time, the single-node USB command above is the repeatable group-flash procedure.
|
||||||
|
|
||||||
|
## Quick Difference: Master vs Node
|
||||||
|
|
||||||
|
- Master:
|
||||||
|
- Target: `rfp_esp32s3_wroom1_n16r8_master`
|
||||||
|
- Typical IP: `192.168.178.10`
|
||||||
|
- Binary: `.pio/build/rfp_esp32s3_wroom1_n16r8_master/firmware.bin`
|
||||||
|
- Node:
|
||||||
|
- Target: `rfp_esp32s3_wroom1_n16r8_3x106`
|
||||||
|
- Typical IPs: `192.168.178.11` to `192.168.178.16`
|
||||||
|
- Binary: `.pio/build/rfp_esp32s3_wroom1_n16r8_3x106/firmware.bin`
|
||||||
|
- Cold-boot test node:
|
||||||
|
- Target: `rfp_esp32s3_wroom1_n16r8_3x106_coldboot`
|
||||||
|
- Binary: `.pio/build/rfp_esp32s3_wroom1_n16r8_3x106_coldboot/firmware.bin`
|
||||||
|
- Cold-boot test master:
|
||||||
|
- Target: `rfp_esp32s3_wroom1_n16r8_master_coldboot`
|
||||||
|
- Binary: `.pio/build/rfp_esp32s3_wroom1_n16r8_master_coldboot/firmware.bin`
|
||||||
|
|
||||||
|
## Recommended Order
|
||||||
|
|
||||||
|
1. Test the affected board with its cold-boot target:
|
||||||
|
- master: `rfp_esp32s3_wroom1_n16r8_master_coldboot`
|
||||||
|
- node: `rfp_esp32s3_wroom1_n16r8_3x106_coldboot`
|
||||||
|
2. Remove power for at least 20 to 30 seconds
|
||||||
|
3. Verify whether it now boots without pressing `RESET`
|
||||||
|
4. If it works, roll the same target to the remaining nodes
|
||||||
|
5. If it still fails, inspect the hardware power-up path on `EN`, 3.3V rail, and any external loads
|
||||||
|
|
||||||
|
## Hand-off für andere (teamfähig)
|
||||||
|
|
||||||
|
Use this section when you want to provide the procedure to other people without hard-coding your local paths.
|
||||||
|
|
||||||
|
### Quickstart Template
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export RFP_REPO="/path/to/WLED-MM/repo"
|
||||||
|
export NODE_BIN="/path/to/node/bin" # optional if node is already in PATH
|
||||||
|
|
||||||
|
cd "$RFP_REPO"
|
||||||
|
|
||||||
|
PATH="$NODE_BIN:$PATH" \
|
||||||
|
NPM_CONFIG_CACHE="$RFP_REPO/.npm-cache" \
|
||||||
|
PLATFORMIO_CORE_DIR="$RFP_REPO/.piohome" \
|
||||||
|
PLATFORMIO_PACKAGES_DIR="$RFP_REPO/.piohome/packages" \
|
||||||
|
PLATFORMIO_PLATFORMS_DIR="$RFP_REPO/.piohome/platforms" \
|
||||||
|
PLATFORMIO_CACHE_DIR="$RFP_REPO/.piohome/.cache" \
|
||||||
|
PLATFORMIO_BUILD_CACHE_DIR="$RFP_REPO/.piohome/buildcache" \
|
||||||
|
.venv/bin/python -m platformio run -e rfp_esp32s3_wroom1_n16r8_3x106
|
||||||
|
```
|
||||||
|
|
||||||
|
### OTA group-flash template
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd "$RFP_REPO"
|
||||||
|
|
||||||
|
.venv/bin/python tools/rfp_network_flash.py flash \
|
||||||
|
--targets <ip1>,<ip2>,<ip3>,<ip4>,<ip5>,<ip6> \
|
||||||
|
--start-from <ip1> \
|
||||||
|
--firmware .pio/build/rfp_esp32s3_wroom1_n16r8_3x106/firmware.bin
|
||||||
|
```
|
||||||
|
|
||||||
|
### Minimal hand-off checklist
|
||||||
|
|
||||||
|
1. Confirm all nodes are reachable in the same network.
|
||||||
|
2. Build once, then flash.
|
||||||
|
3. Validate reboot behavior on each node.
|
||||||
|
4. If one node fails, resume with `--start-from` from that node.
|
||||||
|
5. Document which target was used (`standard` vs `coldboot`).
|
||||||
|
|
||||||
|
### Rollback / removal plan for shared instructions
|
||||||
|
|
||||||
|
1. Revert this doc section in git:
|
||||||
|
- `git checkout -- docs/rfp-node-flashing.md`
|
||||||
|
2. Or remove only the hand-off section manually if the rest should stay.
|
||||||
|
3. If a deployment should be rolled back, flash the previous known-good firmware bin with the same OTA/USB commands.
|
||||||
@@ -24,5 +24,6 @@ applyTo: "wled00/data/**"
|
|||||||
|
|
||||||
## Build Integration
|
## Build Integration
|
||||||
|
|
||||||
Files in this directory are processed by `tools/cdata.js` into `wled00/html_*.h` headers.
|
Files in this directory are processed by `tools/cdata.js` into generated headers
|
||||||
Run `npm run build` after any change. **Never edit the generated `html_*.h` files directly.**
|
(`wled00/html_*.h`, `wled00/js_*.h`).
|
||||||
|
Run `npm run build` after any change. **Never edit generated headers directly.**
|
||||||
@@ -107,6 +107,7 @@ default_envs =
|
|||||||
;; === esp32-S3 === with 16MB flash
|
;; === esp32-S3 === with 16MB flash
|
||||||
esp32S3_16MB_PSRAM_M_HUB75 ;; for S3 with 16MB flash, HUB75 supported (MOONHUB HUB75 adapter board)
|
esp32S3_16MB_PSRAM_M_HUB75 ;; for S3 with 16MB flash, HUB75 supported (MOONHUB HUB75 adapter board)
|
||||||
esp32S3_WROOM-2_M ;; for S3 WROOM-2; HUB75 supported
|
esp32S3_WROOM-2_M ;; for S3 WROOM-2; HUB75 supported
|
||||||
|
; rfp_esp32s3_wroom1_n16r8_3x106 ;; RFP ESP32-S3 WROOM-1 N16R8, 3x106 pixels on GPIO 4/5/6
|
||||||
;;
|
;;
|
||||||
;; === esp32-S2 boards ===
|
;; === esp32-S2 boards ===
|
||||||
esp32s2_PSRAM_S ;; OTA-compatible with upstream
|
esp32s2_PSRAM_S ;; OTA-compatible with upstream
|
||||||
@@ -2438,6 +2439,32 @@ build_flags = ${env:esp32S3_8MB_PSRAM_M_opi.build_flags}
|
|||||||
[env:esp32S3_8MB_PSRAM_M] ;; legacy alias
|
[env:esp32S3_8MB_PSRAM_M] ;; legacy alias
|
||||||
extends = env:esp32S3_8MB_PSRAM_M_opi
|
extends = env:esp32S3_8MB_PSRAM_M_opi
|
||||||
|
|
||||||
|
[env:rfp_esp32s3_wroom1_n16r8_3x106]
|
||||||
|
;; RFP ESP32-S3 WROOM-1 N16R8, 16MB flash / 8MB OPI PSRAM, 3 outputs x 106 pixels
|
||||||
|
extends = env:esp32S3_8MB_PSRAM_M_opi
|
||||||
|
board_upload.flash_size = 16MB
|
||||||
|
board_upload.maximum_size = 16777216
|
||||||
|
board_build.partitions = tools/WLED_ESP32_16MB.csv
|
||||||
|
build_unflags = ${env:esp32S3_8MB_PSRAM_M_opi.build_unflags}
|
||||||
|
-D WLED_RELEASE_NAME=esp32S3_8MB_PSRAM_M_opi
|
||||||
|
-D LEDPIN=21
|
||||||
|
-D BTNPIN=0
|
||||||
|
-D RLYPIN=1
|
||||||
|
-D IRPIN=-1
|
||||||
|
-D AUDIOPIN=-1
|
||||||
|
build_flags = ${env:esp32S3_8MB_PSRAM_M_opi.build_flags}
|
||||||
|
-D WLED_RELEASE_NAME=RFP_ESP32S3_N16R8_3x106
|
||||||
|
-D LEDPIN=4
|
||||||
|
-D DATA_PINS=4,5,6
|
||||||
|
-D PIXEL_COUNTS=106,106,106
|
||||||
|
-D DEFAULT_LED_COUNT=106
|
||||||
|
-D STATUSPIXELPIN=48
|
||||||
|
-D STATUSPIXELCOLORORDER=COL_ORDER_GRB
|
||||||
|
-D BTNPIN=-1
|
||||||
|
-D RLYPIN=-1
|
||||||
|
-D IRPIN=-1
|
||||||
|
-D AUDIOPIN=-1
|
||||||
|
|
||||||
[env:esp32S3_8MB_S]
|
[env:esp32S3_8MB_S]
|
||||||
;; MM for ESP32-S3 boards - FASTPATH + optimize for speed; ; HUB75 support included (may still have pin conflicts)
|
;; MM for ESP32-S3 boards - FASTPATH + optimize for speed; ; HUB75 support included (may still have pin conflicts)
|
||||||
extends = esp32_4MB_V4_M_base
|
extends = esp32_4MB_V4_M_base
|
||||||
|
|||||||
44
tools/flash_rfp_s3.ps1
Normal file
44
tools/flash_rfp_s3.ps1
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
param(
|
||||||
|
[string]$ComPort,
|
||||||
|
|
||||||
|
[switch]$BuildOnly
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||||
|
$repoRoot = Split-Path -Parent $scriptDir
|
||||||
|
$envName = "rfp_esp32s3_wroom1_n16r8_3x106"
|
||||||
|
$venvPython = Join-Path $repoRoot ".venv\Scripts\python.exe"
|
||||||
|
|
||||||
|
if (-not $BuildOnly -and [string]::IsNullOrWhiteSpace($ComPort)) {
|
||||||
|
throw "ComPort is required unless -BuildOnly is used."
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Test-Path $venvPython) {
|
||||||
|
$pythonCommand = $venvPython
|
||||||
|
$pythonArgs = @()
|
||||||
|
} elseif (Get-Command py -ErrorAction SilentlyContinue) {
|
||||||
|
$pythonCommand = "py"
|
||||||
|
$pythonArgs = @("-3")
|
||||||
|
} elseif (Get-Command python -ErrorAction SilentlyContinue) {
|
||||||
|
$pythonCommand = "python"
|
||||||
|
$pythonArgs = @()
|
||||||
|
} else {
|
||||||
|
throw "No Python runtime found. Install Python or create .venv first."
|
||||||
|
}
|
||||||
|
|
||||||
|
$pioHome = Join-Path $repoRoot ".piohome"
|
||||||
|
$env:PLATFORMIO_CORE_DIR = $pioHome
|
||||||
|
$env:PLATFORMIO_PACKAGES_DIR = Join-Path $pioHome "packages"
|
||||||
|
$env:PLATFORMIO_PLATFORMS_DIR = Join-Path $pioHome "platforms"
|
||||||
|
$env:PLATFORMIO_CACHE_DIR = Join-Path $pioHome ".cache"
|
||||||
|
$env:PLATFORMIO_BUILD_CACHE_DIR = Join-Path $pioHome "buildcache"
|
||||||
|
|
||||||
|
$args = @("-m", "platformio", "run", "-e", $envName)
|
||||||
|
if (-not $BuildOnly) {
|
||||||
|
$args += @("-t", "upload", "--upload-port", $ComPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
& $pythonCommand @pythonArgs @args
|
||||||
|
exit $LASTEXITCODE
|
||||||
361
tools/infinity_visualizer_server.py
Normal file
361
tools/infinity_visualizer_server.py
Normal file
@@ -0,0 +1,361 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Local Infinity visualizer for the exact Global-2D layer.
|
||||||
|
|
||||||
|
The browser does not reimplement WLED effects. This server proxies the master
|
||||||
|
state and renders only the Infinity Global-2D layer for the 6 x 3 x 106 layout.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import errno
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
import socket
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import urllib.error
|
||||||
|
import urllib.parse
|
||||||
|
import urllib.request
|
||||||
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
NODE_COUNT = 6
|
||||||
|
ROWS = 3
|
||||||
|
LEDS_PER_PANEL = 106
|
||||||
|
OUTPUT_LABELS = ["UART6", "UART5", "UART4"]
|
||||||
|
MODE_NAMES = ["Off", "Center Pulse", "Checkerd", "Arrow", "Scan", "Snake", "Wave Line"]
|
||||||
|
VARIANT_NAMES = ["Expand / Classic / Line", "Reverse / Diagonal / Bands", "Outline / Checkerd", "Outline Reverse"]
|
||||||
|
BLEND_NAMES = ["Replace", "Add", "Multiply Mask", "Palette Tint"]
|
||||||
|
DIRECTION_NAMES = ["Left -> Right", "Right -> Left", "Top -> Bottom", "Bottom -> Top", "Outward", "Inward", "Ping Pong"]
|
||||||
|
BPM_MIN = 20
|
||||||
|
BPM_MAX = 240
|
||||||
|
|
||||||
|
HTML = r"""<!doctype html>
|
||||||
|
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Infinity Local Visualizer</title>
|
||||||
|
<style>
|
||||||
|
:root{--bg:#0f1115;--panel:#181b22;--line:#303640;--text:#eef2f6;--muted:#9aa5b5;--good:#35d07f;--bad:#ff637d;--warn:#ffd166;--accent:#4da3ff}*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font:14px system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}main{max-width:1320px;margin:0 auto;padding:14px}header{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:12px}h1{margin:0;font-size:22px}.sub,.muted{color:var(--muted)}button,input{font:inherit;border:1px solid var(--line);background:#11151b;color:var(--text);border-radius:6px;padding:8px 10px}button{cursor:pointer;font-weight:650}.toolbar{display:flex;gap:8px;flex-wrap:wrap;align-items:center}.toolbar input{width:170px}.status{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:8px;margin-bottom:12px}.metric,.panel,.log{background:var(--panel);border:1px solid var(--line);border-radius:8px}.metric{padding:10px;min-width:0}.metric span{display:block;color:var(--muted);font-size:11px;text-transform:uppercase}.metric strong{display:block;margin-top:3px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.nodes{display:grid;grid-template-columns:repeat(6,minmax(128px,1fr));gap:12px}.panel{padding:8px;min-width:0}.panel-head{display:flex;justify-content:space-between;gap:8px;margin-bottom:6px;color:var(--muted);font-size:12px}.panel-head strong{color:var(--text)}canvas{width:100%;aspect-ratio:1/1;background:#05070b;border:1px solid #222b35;border-radius:6px;display:block}.log{margin-top:12px;padding:10px;min-height:38px}.ok{color:var(--good)}.bad{color:var(--bad)}.warn{color:var(--warn)}@media(max-width:900px){header{align-items:flex-start;flex-direction:column}.status{grid-template-columns:1fr}.nodes{grid-template-columns:repeat(3,minmax(92px,1fr));gap:8px}.toolbar,.toolbar input{width:100%}button{flex:1 1 auto}}
|
||||||
|
</style></head><body><main>
|
||||||
|
<header><div><h1>Infinity Local Visualizer</h1><div class="sub">Exact Global-2D layer preview. WLED base effects are not simulated.</div></div><div class="toolbar"><input id="master" aria-label="Master IP"><button id="apply">Connect</button><button id="pause">Pause</button><a id="masterLink" href="#" target="_blank"><button type="button">Master UI</button></a></div></header>
|
||||||
|
<section class="status" id="status"></section><section class="nodes" id="nodes"></section><div class="log" id="log">Starting...</div>
|
||||||
|
<script>
|
||||||
|
const NODE_COUNT=6, ROWS=3, LEDS=106, OUTPUT_LABELS=["UART6","UART5","UART4"];
|
||||||
|
let frame=null, paused=false, refreshInFlight=false;const q=id=>document.getElementById(id);const masterIp=()=>q("master").value.trim()||new URLSearchParams(location.search).get("master")||"10.42.0.213";
|
||||||
|
function ensureNodes(){if(q("nodes").children.length)return;q("nodes").innerHTML=Array.from({length:ROWS},(_,row)=>Array.from({length:NODE_COUNT},(_,node)=>`<article class="panel"><div class="panel-head"><strong>ESP${node+1} ${OUTPUT_LABELS[row]}</strong><span id="meta${node}_${row}">106 LEDs</span></div><canvas width="160" height="160" id="c${node}_${row}"></canvas></article>`).join("")).join("")}
|
||||||
|
function drawPanel(canvas, leds){const ctx=canvas.getContext("2d");ctx.clearRect(0,0,160,160);ctx.fillStyle="#05070b";ctx.fillRect(0,0,160,160);ctx.strokeStyle="#1e2732";ctx.lineWidth=2;ctx.strokeRect(23,23,114,114);for(let i=0;i<leds.length;i++){let x=0,y=0;if(i<25){x=26+i*(108/24);y=24}else if(i<52){x=136;y=26+(i-25)*(108/26)}else if(i<79){x=136-(i-52)*(108/26);y=136}else{x=24;y=136-(i-79)*(108/26)}const c=leds[i];ctx.fillStyle=`rgb(${c[0]},${c[1]},${c[2]})`;ctx.beginPath();ctx.arc(x,y,2.1,0,Math.PI*2);ctx.fill()}}
|
||||||
|
function render(){ensureNodes();const s=frame?.scene||{}, spatial=s.spatial||{};for(let row=0;row<ROWS;row++){for(let node=0;node<NODE_COUNT;node++){q(`meta${node}_${row}`).textContent=frame?.node_ips?.[node]||"virtual";drawPanel(q(`c${node}_${row}`),frame?.panels?.[row]?.[node]||Array.from({length:LEDS},()=>[0,0,0]));}}q("status").innerHTML=[["Master",masterIp()],["2D Mode",frame?.mode_name||"Off"],["Variant",frame?.variant_name||"-"],["Direction",frame?.direction_name||"-"],["Blend",frame?.blend_name||"-"],["Layer",frame?.note||"2D layer exact"]].map(([k,v])=>`<div class="metric"><span>${k}</span><strong>${v}</strong></div>`).join("")}
|
||||||
|
async function refresh(){if(paused||refreshInFlight)return;refreshInFlight=true;const ip=masterIp();q("masterLink").href=`http://${ip}/infinity`;try{const response=await fetch(`/api/frame?master=${encodeURIComponent(ip)}`,{cache:"no-store"});if(!response.ok)throw new Error((await response.text()).replace(/[{}\"]/g,""));frame=await response.json();q("log").innerHTML=`<span class="ok">connected</span> ${new Date().toLocaleTimeString()} via ${ip}. <span class="warn">WLED base not simulated; showing exact Infinity 2D layer only.</span>`;render()}catch(error){q("log").innerHTML=`<span class="bad">master offline</span> ${ip} · ${error.message}`}finally{refreshInFlight=false}}
|
||||||
|
q("master").value=masterIp();q("apply").onclick=refresh;q("pause").onclick=()=>{paused=!paused;q("pause").textContent=paused?"Resume":"Pause"};setInterval(refresh,250);refresh();
|
||||||
|
</script></main></body></html>"""
|
||||||
|
|
||||||
|
|
||||||
|
def clamp_byte(value: float) -> int:
|
||||||
|
return max(0, min(255, int(round(value))))
|
||||||
|
|
||||||
|
|
||||||
|
def smoothstep(edge0: float, edge1: float, x: float) -> float:
|
||||||
|
if edge0 == edge1:
|
||||||
|
return 0.0 if x < edge0 else 1.0
|
||||||
|
x = max(0.0, min(1.0, (x - edge0) / (edge1 - edge0)))
|
||||||
|
return x * x * (3.0 - 2.0 * x)
|
||||||
|
|
||||||
|
|
||||||
|
def speed_to_bpm(speed: int) -> int:
|
||||||
|
speed = max(0, min(255, int(speed)))
|
||||||
|
return round(BPM_MIN + speed * (BPM_MAX - BPM_MIN) / 255.0)
|
||||||
|
|
||||||
|
|
||||||
|
def spatial_phase(now_us: int, speed: int) -> float:
|
||||||
|
seconds = (now_us % 60_000_000) / 1_000_000.0
|
||||||
|
cps = speed_to_bpm(speed) / 60.0
|
||||||
|
return seconds * cps
|
||||||
|
|
||||||
|
|
||||||
|
def panel_led_position(led: int) -> tuple[float, float, int]:
|
||||||
|
if led < 25:
|
||||||
|
return (led + 0.5) / 25.0, 0.0, 0
|
||||||
|
if led < 52:
|
||||||
|
return 1.0, (led - 25 + 0.5) / 27.0, 1
|
||||||
|
if led < 79:
|
||||||
|
return 1.0 - ((led - 52 + 0.5) / 27.0), 1.0, 2
|
||||||
|
return 0.0, 1.0 - ((led - 79 + 0.5) / 27.0), 3
|
||||||
|
|
||||||
|
|
||||||
|
def center_pulse_amount(col: int, row: int, led: int, now_us: int, speed: int, variant: int) -> int:
|
||||||
|
distance = abs(row - 1.0) + abs(col - 2.5)
|
||||||
|
max_distance = 3.5
|
||||||
|
span = max_distance + 1.0
|
||||||
|
front = (spatial_phase(now_us, speed) * span) % span
|
||||||
|
if variant in (1, 3):
|
||||||
|
front = max_distance - front
|
||||||
|
amount = 1.0 - smoothstep(0.0, 0.70, abs(distance - front))
|
||||||
|
value = clamp_byte(amount * 255.0)
|
||||||
|
if variant in (2, 3):
|
||||||
|
_, _, side = panel_led_position(led)
|
||||||
|
if row == 1 and col in (2, 3):
|
||||||
|
return value if side in (0, 2) else 0
|
||||||
|
if col == 0:
|
||||||
|
return value if side == 3 else 0
|
||||||
|
if col == 5:
|
||||||
|
return value if side == 1 else 0
|
||||||
|
if row == 0:
|
||||||
|
return value if side == 0 else 0
|
||||||
|
if row == 2:
|
||||||
|
return value if side == 2 else 0
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def checker_amount(col: int, row: int, led: int, now_us: int, speed: int, variant: int) -> int:
|
||||||
|
parity = (row + col) & 1
|
||||||
|
step = int(math.floor(spatial_phase(now_us, speed)))
|
||||||
|
if variant in (1, 2):
|
||||||
|
x, y, _ = panel_led_position(led)
|
||||||
|
slash = variant == 2 and (step & 1)
|
||||||
|
first = y <= (1.0 - x if slash else x)
|
||||||
|
return 255 if ((parity == 0) == first) else 0
|
||||||
|
return 255 if ((parity + step) & 1) == 0 else 0
|
||||||
|
|
||||||
|
|
||||||
|
def wave_line_amount(col: int, row: int, now_us: int, speed: int, direction: int) -> int:
|
||||||
|
triangle = [0, 1, 2, 1]
|
||||||
|
step = int(math.floor(spatial_phase(now_us, speed)))
|
||||||
|
if direction in (2, 3):
|
||||||
|
phase = step if direction == 2 else -step
|
||||||
|
target = round(triangle[(row - phase) % 4] * ((NODE_COUNT - 1) / 2.0) / 2.0)
|
||||||
|
return 255 if col == min(target, NODE_COUNT - 1) else 0
|
||||||
|
phase = -step if direction == 1 else step
|
||||||
|
target = round(triangle[(col - phase) % 4] * ((ROWS - 1) / 2.0))
|
||||||
|
return 255 if row == min(target, ROWS - 1) else 0
|
||||||
|
|
||||||
|
|
||||||
|
def arrow_amount(col: int, row: int, now_us: int, speed: int, direction: int, size: int) -> int:
|
||||||
|
horizontal = direction not in (2, 3)
|
||||||
|
major_count = NODE_COUNT if horizontal else ROWS
|
||||||
|
minor_count = ROWS if horizontal else NODE_COUNT
|
||||||
|
major = col if horizontal else row
|
||||||
|
minor = row if horizontal else col
|
||||||
|
gap = max(1, 1 + size // 86) - 1
|
||||||
|
span = 3 + gap
|
||||||
|
movement = int(math.floor(spatial_phase(now_us, speed)))
|
||||||
|
band = 0 if abs(minor - ((minor_count - 1) / 2.0)) <= 0.55 else 1
|
||||||
|
orientation_right = direction in (0, 2, 4)
|
||||||
|
target = 1 if band == 0 else (0 if orientation_right else 2)
|
||||||
|
local = major - movement if orientation_right else major + movement
|
||||||
|
return 255 if major_count > 0 and (local % span) == target else 0
|
||||||
|
|
||||||
|
|
||||||
|
def scan_amount(col: int, row: int, led: int, now_us: int, speed: int, size: int, angle: int, option: int, direction: int) -> int:
|
||||||
|
x, y, _ = panel_led_position(led)
|
||||||
|
vertical = direction in (2, 3)
|
||||||
|
radians = math.radians((angle + (90 if vertical else 0)) % 360)
|
||||||
|
vx, vy = math.cos(radians), math.sin(radians)
|
||||||
|
progress = (col + x) * vx + (row + y) * vy
|
||||||
|
min_progress, max_progress = -3.0, 8.0
|
||||||
|
width = 0.15 + (size / 255.0) * (1.60 if option == 1 else 0.85)
|
||||||
|
travel = (max_progress - min_progress) + width
|
||||||
|
phase = (spatial_phase(now_us, speed) * travel) % travel
|
||||||
|
if direction == 6:
|
||||||
|
phase = (spatial_phase(now_us, speed) * travel) % (travel * 2.0)
|
||||||
|
if phase > travel:
|
||||||
|
phase = (travel * 2.0) - phase
|
||||||
|
elif direction in (1, 3):
|
||||||
|
phase = travel - phase
|
||||||
|
center = min_progress + phase
|
||||||
|
if option == 1:
|
||||||
|
period = width * 2.0 + 0.35
|
||||||
|
d = abs(((progress - center + period * 64.0) % period) - period * 0.5)
|
||||||
|
return clamp_byte((1.0 - smoothstep(width * 0.45, width * 0.75, d)) * 255.0)
|
||||||
|
return clamp_byte((1.0 - smoothstep(width * 0.5, width * 0.5 + 0.55, abs(progress - center))) * 255.0)
|
||||||
|
|
||||||
|
|
||||||
|
def snake_amount(col: int, row: int, now_us: int, speed: int, size: int, seed: int) -> int:
|
||||||
|
path_len = NODE_COUNT * ROWS
|
||||||
|
panel_index = row * NODE_COUNT + (NODE_COUNT - 1 - col if row & 1 else col)
|
||||||
|
step = int(math.floor(spatial_phase(now_us, speed)))
|
||||||
|
head = (step + seed % path_len) % path_len
|
||||||
|
length = 3 + max(1, size // 64)
|
||||||
|
for offset in range(length):
|
||||||
|
if panel_index == (head + path_len - (offset % path_len)) % path_len:
|
||||||
|
return max(55, 255 - offset * 38)
|
||||||
|
apple = (seed * 17 + (step // path_len) * 11 + 7) % path_len
|
||||||
|
return 180 if panel_index == apple else 0
|
||||||
|
|
||||||
|
|
||||||
|
def blend(primary: list[int], secondary: list[int], amount: int) -> list[int]:
|
||||||
|
return [clamp_byte(secondary[i] + (primary[i] - secondary[i]) * amount / 255.0) for i in range(3)]
|
||||||
|
|
||||||
|
|
||||||
|
def layer_amount(mode: int, col: int, row: int, led: int, now_us: int, spatial: dict[str, Any], scene: dict[str, Any]) -> int:
|
||||||
|
speed = int(scene.get("speed", 128))
|
||||||
|
variant = int(spatial.get("variant", 0))
|
||||||
|
direction = int(spatial.get("direction", 0))
|
||||||
|
size = int(spatial.get("size", 64))
|
||||||
|
if mode == 1:
|
||||||
|
return center_pulse_amount(col, row, led, now_us, speed, variant)
|
||||||
|
if mode == 2:
|
||||||
|
return checker_amount(col, row, led, now_us, speed, variant)
|
||||||
|
if mode == 3:
|
||||||
|
return arrow_amount(col, row, now_us, speed, direction, size)
|
||||||
|
if mode == 4:
|
||||||
|
return scan_amount(col, row, led, now_us, speed, size, int(spatial.get("angle", 0)), int(spatial.get("option", 0)), direction)
|
||||||
|
if mode == 5:
|
||||||
|
return snake_amount(col, row, now_us, speed, size, int(scene.get("seed", 1)))
|
||||||
|
if mode == 6:
|
||||||
|
return wave_line_amount(col, row, now_us, speed, direction)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def render_frame(state: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
scene = state.get("scene", {})
|
||||||
|
spatial = scene.get("spatial", {}) or {}
|
||||||
|
mode = int(spatial.get("mode", 0))
|
||||||
|
strength = int(spatial.get("strength", 180))
|
||||||
|
primary = scene.get("primary", [255, 160, 80])[:3]
|
||||||
|
secondary = scene.get("secondary", [0, 32, 255])[:3]
|
||||||
|
now_us = int(time.monotonic() * 1_000_000) + int(scene.get("phase", 0)) * 1000
|
||||||
|
panels: list[list[list[list[int]]]] = []
|
||||||
|
for row in range(ROWS):
|
||||||
|
row_panels = []
|
||||||
|
for col in range(NODE_COUNT):
|
||||||
|
leds = []
|
||||||
|
for led in range(LEDS_PER_PANEL):
|
||||||
|
amount = layer_amount(mode, col, row, led, now_us, spatial, scene)
|
||||||
|
amount = clamp_byte(amount * strength / 255.0)
|
||||||
|
leds.append(blend(primary, secondary, amount) if amount else [0, 0, 0])
|
||||||
|
row_panels.append(leds)
|
||||||
|
panels.append(row_panels)
|
||||||
|
return {
|
||||||
|
"scene": scene,
|
||||||
|
"node_ips": state.get("node_ips", []),
|
||||||
|
"panels": panels,
|
||||||
|
"mode_name": MODE_NAMES[mode] if 0 <= mode < len(MODE_NAMES) else "Unknown",
|
||||||
|
"variant_name": VARIANT_NAMES[int(spatial.get("variant", 0))] if int(spatial.get("variant", 0)) < len(VARIANT_NAMES) else "Unknown",
|
||||||
|
"blend_name": BLEND_NAMES[int(spatial.get("blend", 2))] if int(spatial.get("blend", 2)) < len(BLEND_NAMES) else "Unknown",
|
||||||
|
"direction_name": DIRECTION_NAMES[int(spatial.get("direction", 0))] if int(spatial.get("direction", 0)) < len(DIRECTION_NAMES) else "Unknown",
|
||||||
|
"note": "2D layer exact; WLED base not simulated",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class VisualizerServer(ThreadingHTTPServer):
|
||||||
|
allow_reuse_address = True
|
||||||
|
master: str
|
||||||
|
timeout_s: float
|
||||||
|
|
||||||
|
|
||||||
|
class Handler(BaseHTTPRequestHandler):
|
||||||
|
server: VisualizerServer
|
||||||
|
|
||||||
|
def log_message(self, fmt: str, *args: Any) -> None:
|
||||||
|
message = fmt % args
|
||||||
|
if '"GET /api/' in message and (' 502 ' in message or ' 504 ' in message):
|
||||||
|
return
|
||||||
|
sys.stderr.write("%s - %s\n" % (self.log_date_time_string(), message))
|
||||||
|
|
||||||
|
def send_bytes(self, status: int, content_type: str, body: bytes) -> None:
|
||||||
|
try:
|
||||||
|
self.send_response(status)
|
||||||
|
self.send_header("Content-Type", content_type)
|
||||||
|
self.send_header("Content-Length", str(len(body)))
|
||||||
|
self.send_header("Cache-Control", "no-store")
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(body)
|
||||||
|
except (BrokenPipeError, ConnectionResetError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def do_GET(self) -> None:
|
||||||
|
if self.path == "/" or self.path.startswith("/?"):
|
||||||
|
self.send_bytes(200, "text/html; charset=utf-8", HTML.encode("utf-8"))
|
||||||
|
return
|
||||||
|
if self.path.startswith("/api/infinity"):
|
||||||
|
self.proxy_infinity(self.master_from_query())
|
||||||
|
return
|
||||||
|
if self.path.startswith("/api/frame"):
|
||||||
|
self.proxy_frame(self.master_from_query())
|
||||||
|
return
|
||||||
|
if self.path == "/health":
|
||||||
|
self.send_bytes(200, "application/json", b'{"ok":true}')
|
||||||
|
return
|
||||||
|
self.send_bytes(404, "text/plain; charset=utf-8", b"not found")
|
||||||
|
|
||||||
|
def master_from_query(self) -> str:
|
||||||
|
values = urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query)
|
||||||
|
return values.get("master", [self.server.master])[0].strip() or self.server.master
|
||||||
|
|
||||||
|
def fetch_master_state(self, master: str) -> dict[str, Any]:
|
||||||
|
url = f"http://{master}/json/infinity"
|
||||||
|
with urllib.request.urlopen(url, timeout=self.server.timeout_s) as response:
|
||||||
|
body = response.read()
|
||||||
|
return json.loads(body.decode("utf-8"))
|
||||||
|
|
||||||
|
def proxy_infinity(self, master: str) -> None:
|
||||||
|
try:
|
||||||
|
state = self.fetch_master_state(master)
|
||||||
|
except (urllib.error.URLError, socket.timeout, TimeoutError) as exc:
|
||||||
|
self.send_bytes(504, "application/json", json.dumps({"error":"master unreachable","detail":str(exc)}).encode("utf-8"))
|
||||||
|
return
|
||||||
|
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||||
|
self.send_bytes(502, "application/json", json.dumps({"error":f"invalid master JSON: {exc}"}).encode("utf-8"))
|
||||||
|
return
|
||||||
|
self.send_bytes(200, "application/json", json.dumps(state).encode("utf-8"))
|
||||||
|
|
||||||
|
def proxy_frame(self, master: str) -> None:
|
||||||
|
try:
|
||||||
|
state = self.fetch_master_state(master)
|
||||||
|
frame = render_frame(state)
|
||||||
|
except (urllib.error.URLError, socket.timeout, TimeoutError) as exc:
|
||||||
|
self.send_bytes(504, "application/json", json.dumps({"error":"master unreachable","detail":str(exc)}).encode("utf-8"))
|
||||||
|
return
|
||||||
|
except Exception as exc: # keep the operator UI alive and explicit
|
||||||
|
self.send_bytes(502, "application/json", json.dumps({"error":f"frame render failed: {exc}"}).encode("utf-8"))
|
||||||
|
return
|
||||||
|
self.send_bytes(200, "application/json", json.dumps(frame, separators=(",", ":")).encode("utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args() -> argparse.Namespace:
|
||||||
|
parser = argparse.ArgumentParser(description="Serve a local Infinity Global-2D visualizer.")
|
||||||
|
parser.add_argument("--master", default="10.42.0.213", help="Infinity master IP or hostname")
|
||||||
|
parser.add_argument("--bind", default="127.0.0.1", help="Local bind address")
|
||||||
|
parser.add_argument("--port", type=int, default=8765, help="Local HTTP port")
|
||||||
|
parser.add_argument("--no-port-fallback", action="store_true", help="Fail instead of trying the next ports when busy")
|
||||||
|
parser.add_argument("--timeout", type=float, default=1.2, help="Master request timeout in seconds")
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def bind_server(args: argparse.Namespace) -> VisualizerServer:
|
||||||
|
last_error: OSError | None = None
|
||||||
|
ports = [args.port] if args.no_port_fallback else range(args.port, args.port + 50)
|
||||||
|
for port in ports:
|
||||||
|
try:
|
||||||
|
server = VisualizerServer((args.bind, port), Handler)
|
||||||
|
if port != args.port:
|
||||||
|
print(f"Port {args.port} is busy, using {port} instead.")
|
||||||
|
return server
|
||||||
|
except OSError as exc:
|
||||||
|
last_error = exc
|
||||||
|
if exc.errno != errno.EADDRINUSE or args.no_port_fallback:
|
||||||
|
break
|
||||||
|
if last_error and last_error.errno == errno.EADDRINUSE:
|
||||||
|
raise SystemExit(f"Could not start visualizer: ports {args.port}-{args.port + 49} are busy.")
|
||||||
|
raise last_error or RuntimeError("Could not bind visualizer server")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
args = parse_args()
|
||||||
|
server = bind_server(args)
|
||||||
|
server.master = args.master
|
||||||
|
server.timeout_s = args.timeout
|
||||||
|
host, port = server.server_address[:2]
|
||||||
|
print(f"Infinity visualizer: http://{host}:{port}/?master={args.master}")
|
||||||
|
print(f"Proxying master: http://{args.master}/json/infinity")
|
||||||
|
print("Rendering exact Infinity Global-2D layer; WLED base effects are not simulated.")
|
||||||
|
try:
|
||||||
|
server.serve_forever()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\nStopped.")
|
||||||
|
finally:
|
||||||
|
server.server_close()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
388
tools/rfp_network_flash.py
Executable file
388
tools/rfp_network_flash.py
Executable file
@@ -0,0 +1,388 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Discover WLED devices in local networks and flash them sequentially via OTA.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import ipaddress
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Iterable
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_OUTPUT_FILE = "tools/discovered_wled_hosts.txt"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class WledHost:
|
||||||
|
ip: str
|
||||||
|
name: str
|
||||||
|
version: str
|
||||||
|
arch: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class WledInfo(WledHost):
|
||||||
|
uptime_s: int
|
||||||
|
|
||||||
|
|
||||||
|
def _run(cmd: list[str]) -> str:
|
||||||
|
proc = subprocess.run(cmd, check=True, capture_output=True, text=True)
|
||||||
|
return proc.stdout
|
||||||
|
|
||||||
|
|
||||||
|
def local_networks() -> list[ipaddress.IPv4Network]:
|
||||||
|
"""
|
||||||
|
Read active IPv4 interfaces from `ip` and return private subnets.
|
||||||
|
"""
|
||||||
|
out = _run(["ip", "-o", "-4", "addr", "show", "scope", "global"])
|
||||||
|
nets: list[ipaddress.IPv4Network] = []
|
||||||
|
seen: set[str] = set()
|
||||||
|
for line in out.splitlines():
|
||||||
|
match = re.search(r"\binet\s+(\d+\.\d+\.\d+\.\d+/\d+)\b", line)
|
||||||
|
if not match:
|
||||||
|
continue
|
||||||
|
iface_cidr = match.group(1)
|
||||||
|
iface_ip = ipaddress.ip_interface(iface_cidr)
|
||||||
|
net = iface_ip.network
|
||||||
|
if not iface_ip.ip.is_private:
|
||||||
|
continue
|
||||||
|
if net.num_addresses > 2048:
|
||||||
|
# avoid accidentally scanning very large ranges by default
|
||||||
|
net = ipaddress.ip_network(f"{iface_ip.ip}/24", strict=False)
|
||||||
|
key = str(net)
|
||||||
|
if key in seen:
|
||||||
|
continue
|
||||||
|
seen.add(key)
|
||||||
|
nets.append(net)
|
||||||
|
return nets
|
||||||
|
|
||||||
|
|
||||||
|
def parse_networks(raw: Iterable[str] | None) -> list[ipaddress.IPv4Network]:
|
||||||
|
if raw:
|
||||||
|
nets: list[ipaddress.IPv4Network] = []
|
||||||
|
for item in raw:
|
||||||
|
nets.append(ipaddress.ip_network(item, strict=False))
|
||||||
|
return nets
|
||||||
|
return local_networks()
|
||||||
|
|
||||||
|
|
||||||
|
def probe_wled_info(ip: str, timeout_s: float) -> WledInfo | None:
|
||||||
|
url = f"http://{ip}/json/info"
|
||||||
|
try:
|
||||||
|
resp = requests.get(url, timeout=timeout_s)
|
||||||
|
if resp.status_code != 200:
|
||||||
|
return None
|
||||||
|
data = resp.json()
|
||||||
|
except (requests.RequestException, ValueError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# WLED info endpoint typically includes "name", "ver", and "arch"
|
||||||
|
ver = str(data.get("ver", "")).strip()
|
||||||
|
name = str(data.get("name", "")).strip()
|
||||||
|
arch = str(data.get("arch", "")).strip()
|
||||||
|
if not ver:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not name:
|
||||||
|
name = "WLED"
|
||||||
|
if not arch:
|
||||||
|
arch = "-"
|
||||||
|
|
||||||
|
uptime_s = int(data.get("uptime", 0) or 0)
|
||||||
|
return WledInfo(ip=ip, name=name, version=ver, arch=arch, uptime_s=uptime_s)
|
||||||
|
|
||||||
|
|
||||||
|
def probe_wled(ip: str, timeout_s: float) -> WledHost | None:
|
||||||
|
info = probe_wled_info(ip, timeout_s)
|
||||||
|
if info is None:
|
||||||
|
return None
|
||||||
|
return WledHost(ip=info.ip, name=info.name, version=info.version, arch=info.arch)
|
||||||
|
|
||||||
|
|
||||||
|
def discover_hosts(
|
||||||
|
nets: list[ipaddress.IPv4Network],
|
||||||
|
timeout_s: float,
|
||||||
|
workers: int,
|
||||||
|
) -> list[WledHost]:
|
||||||
|
candidates: list[str] = []
|
||||||
|
for net in nets:
|
||||||
|
for host in net.hosts():
|
||||||
|
candidates.append(str(host))
|
||||||
|
|
||||||
|
found: list[WledHost] = []
|
||||||
|
with ThreadPoolExecutor(max_workers=workers) as executor:
|
||||||
|
futures = [executor.submit(probe_wled, ip, timeout_s) for ip in candidates]
|
||||||
|
for fut in as_completed(futures):
|
||||||
|
result = fut.result()
|
||||||
|
if result is not None:
|
||||||
|
found.append(result)
|
||||||
|
|
||||||
|
found.sort(key=lambda h: tuple(int(part) for part in h.ip.split(".")))
|
||||||
|
return found
|
||||||
|
|
||||||
|
|
||||||
|
def read_targets_file(path: Path) -> list[str]:
|
||||||
|
targets: list[str] = []
|
||||||
|
for raw in path.read_text(encoding="utf-8").splitlines():
|
||||||
|
line = raw.strip()
|
||||||
|
if not line or line.startswith("#"):
|
||||||
|
continue
|
||||||
|
targets.append(line.split()[0])
|
||||||
|
return targets
|
||||||
|
|
||||||
|
|
||||||
|
def write_discovery(path: Path, hosts: list[WledHost]) -> None:
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
lines = ["# ip name version arch"]
|
||||||
|
lines += [f"{h.ip} {h.name} {h.version} {h.arch}" for h in hosts]
|
||||||
|
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def wait_for_online(ip: str, timeout_s: float, interval_s: float) -> bool:
|
||||||
|
deadline = time.time() + timeout_s
|
||||||
|
while time.time() < deadline:
|
||||||
|
if probe_wled(ip, timeout_s=1.2) is not None:
|
||||||
|
return True
|
||||||
|
time.sleep(interval_s)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def wait_for_offline(ip: str, timeout_s: float, interval_s: float) -> bool:
|
||||||
|
deadline = time.time() + timeout_s
|
||||||
|
while time.time() < deadline:
|
||||||
|
if probe_wled(ip, timeout_s=1.2) is None:
|
||||||
|
return True
|
||||||
|
time.sleep(interval_s)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def wait_for_online_info(ip: str, timeout_s: float, interval_s: float) -> WledInfo | None:
|
||||||
|
deadline = time.time() + timeout_s
|
||||||
|
while time.time() < deadline:
|
||||||
|
info = probe_wled_info(ip, timeout_s=1.2)
|
||||||
|
if info is not None:
|
||||||
|
return info
|
||||||
|
time.sleep(interval_s)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def reboot_confirmed(before: WledInfo | None, after: WledInfo, offline_seen: bool) -> tuple[bool, str]:
|
||||||
|
if offline_seen:
|
||||||
|
return True, "offline transition observed"
|
||||||
|
if before is None:
|
||||||
|
return False, "device was not profiled before upload, and no offline transition was observed"
|
||||||
|
if after.uptime_s + 5 < before.uptime_s:
|
||||||
|
return True, f"uptime reset from {before.uptime_s}s to {after.uptime_s}s"
|
||||||
|
return False, f"device stayed reachable and uptime did not reset ({before.uptime_s}s -> {after.uptime_s}s)"
|
||||||
|
|
||||||
|
|
||||||
|
def ota_flash(ip: str, firmware: Path, connect_timeout_s: float, read_timeout_s: float) -> tuple[str, str]:
|
||||||
|
url = f"http://{ip}/update"
|
||||||
|
try:
|
||||||
|
with firmware.open("rb") as fh:
|
||||||
|
resp = requests.post(
|
||||||
|
url,
|
||||||
|
files={"update": (firmware.name, fh, "application/octet-stream")},
|
||||||
|
timeout=(connect_timeout_s, read_timeout_s),
|
||||||
|
)
|
||||||
|
except requests.ReadTimeout:
|
||||||
|
# Common with WLED OTA: upload is accepted but HTTP response never arrives before reboot.
|
||||||
|
return "uncertain", "read timeout after upload"
|
||||||
|
except requests.ConnectionError:
|
||||||
|
# Some devices close the socket abruptly when rebooting after successful OTA.
|
||||||
|
return "uncertain", "connection dropped during/after upload"
|
||||||
|
except requests.RequestException as exc:
|
||||||
|
return "failed", f"request failed: {exc}"
|
||||||
|
|
||||||
|
text = (resp.text or "").lower()
|
||||||
|
if resp.status_code >= 400:
|
||||||
|
return "failed", f"http {resp.status_code}"
|
||||||
|
if "fail" in text or "error" in text:
|
||||||
|
return "failed", "device reported update failure"
|
||||||
|
return "ok", "ok"
|
||||||
|
|
||||||
|
|
||||||
|
def print_hosts(hosts: list[WledHost]) -> None:
|
||||||
|
if not hosts:
|
||||||
|
print("No WLED devices found.")
|
||||||
|
return
|
||||||
|
print(f"Found {len(hosts)} WLED device(s):")
|
||||||
|
print(f"{'IP':<16} {'Name':<24} {'Version':<18} {'Arch'}")
|
||||||
|
print("-" * 80)
|
||||||
|
for h in hosts:
|
||||||
|
print(f"{h.ip:<16} {h.name[:24]:<24} {h.version[:18]:<18} {h.arch}")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_discover(args: argparse.Namespace) -> int:
|
||||||
|
nets = parse_networks(args.subnet)
|
||||||
|
if not nets:
|
||||||
|
print("No private IPv4 networks found. Pass --subnet explicitly.")
|
||||||
|
return 2
|
||||||
|
|
||||||
|
print("Scanning networks:", ", ".join(str(n) for n in nets))
|
||||||
|
hosts = discover_hosts(nets=nets, timeout_s=args.timeout, workers=args.workers)
|
||||||
|
print_hosts(hosts)
|
||||||
|
|
||||||
|
if args.output:
|
||||||
|
out = Path(args.output)
|
||||||
|
write_discovery(out, hosts)
|
||||||
|
print(f"Saved discovery list: {out}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_targets(args: argparse.Namespace) -> list[str]:
|
||||||
|
targets: list[str] = []
|
||||||
|
|
||||||
|
if args.targets:
|
||||||
|
targets.extend([t.strip() for t in args.targets.split(",") if t.strip()])
|
||||||
|
|
||||||
|
if args.targets_file:
|
||||||
|
targets.extend(read_targets_file(Path(args.targets_file)))
|
||||||
|
|
||||||
|
if args.discover:
|
||||||
|
nets = parse_networks(args.subnet)
|
||||||
|
hosts = discover_hosts(nets=nets, timeout_s=args.timeout, workers=args.workers)
|
||||||
|
targets.extend([h.ip for h in hosts])
|
||||||
|
|
||||||
|
unique: list[str] = []
|
||||||
|
seen: set[str] = set()
|
||||||
|
for t in targets:
|
||||||
|
if t in seen:
|
||||||
|
continue
|
||||||
|
seen.add(t)
|
||||||
|
unique.append(t)
|
||||||
|
return unique
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_flash(args: argparse.Namespace) -> int:
|
||||||
|
firmware = Path(args.firmware)
|
||||||
|
if not firmware.exists():
|
||||||
|
print(f"Firmware file not found: {firmware}")
|
||||||
|
return 2
|
||||||
|
|
||||||
|
targets = resolve_targets(args)
|
||||||
|
if not targets:
|
||||||
|
print("No targets selected. Use --targets, --targets-file, or --discover.")
|
||||||
|
return 2
|
||||||
|
if args.start_from:
|
||||||
|
if args.start_from not in targets:
|
||||||
|
print(f"--start-from target not found in target set: {args.start_from}")
|
||||||
|
return 2
|
||||||
|
start_idx = targets.index(args.start_from)
|
||||||
|
targets = targets[start_idx:]
|
||||||
|
|
||||||
|
print(f"Flashing {len(targets)} device(s) sequentially with: {firmware}")
|
||||||
|
failures: list[str] = []
|
||||||
|
|
||||||
|
for idx, ip in enumerate(targets, start=1):
|
||||||
|
before = probe_wled_info(ip, timeout_s=args.timeout)
|
||||||
|
if before is not None:
|
||||||
|
print(
|
||||||
|
f"[{idx}/{len(targets)}] {ip}: current firmware {before.version}, "
|
||||||
|
f"uptime {before.uptime_s}s, name '{before.name}'"
|
||||||
|
)
|
||||||
|
print(f"[{idx}/{len(targets)}] {ip}: uploading...")
|
||||||
|
status, msg = ota_flash(
|
||||||
|
ip=ip,
|
||||||
|
firmware=firmware,
|
||||||
|
connect_timeout_s=args.connect_timeout,
|
||||||
|
read_timeout_s=args.upload_timeout,
|
||||||
|
)
|
||||||
|
if status == "failed":
|
||||||
|
print(f"[{idx}/{len(targets)}] {ip}: FAILED ({msg})")
|
||||||
|
failures.append(ip)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if status == "uncertain":
|
||||||
|
print(f"[{idx}/{len(targets)}] {ip}: upload response uncertain ({msg}), verifying via reboot check...")
|
||||||
|
else:
|
||||||
|
print(f"[{idx}/{len(targets)}] {ip}: uploaded, waiting {args.reboot_wait:.1f}s for reboot...")
|
||||||
|
time.sleep(args.reboot_wait)
|
||||||
|
|
||||||
|
offline_seen = wait_for_offline(ip=ip, timeout_s=args.offline_timeout, interval_s=0.5)
|
||||||
|
if offline_seen:
|
||||||
|
print(f"[{idx}/{len(targets)}] {ip}: reboot detected, device went offline.")
|
||||||
|
else:
|
||||||
|
print(f"[{idx}/{len(targets)}] {ip}: warning, no offline transition observed. Checking uptime reset...")
|
||||||
|
|
||||||
|
after = wait_for_online_info(ip=ip, timeout_s=args.online_timeout, interval_s=1.0)
|
||||||
|
if after is None:
|
||||||
|
print(f"[{idx}/{len(targets)}] {ip}: no online response after upload window.")
|
||||||
|
failures.append(ip)
|
||||||
|
continue
|
||||||
|
|
||||||
|
reboot_ok, reason = reboot_confirmed(before=before, after=after, offline_seen=offline_seen)
|
||||||
|
if not reboot_ok:
|
||||||
|
print(f"[{idx}/{len(targets)}] {ip}: FAILED (could not prove reboot: {reason})")
|
||||||
|
failures.append(ip)
|
||||||
|
continue
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"[{idx}/{len(targets)}] {ip}: OK "
|
||||||
|
f"(now {after.version}, uptime {after.uptime_s}s, {reason})"
|
||||||
|
)
|
||||||
|
|
||||||
|
if failures:
|
||||||
|
print("\nFailed targets:")
|
||||||
|
for ip in failures:
|
||||||
|
print(f"- {ip}")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
print("\nAll targets flashed successfully.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def build_parser() -> argparse.ArgumentParser:
|
||||||
|
parser = argparse.ArgumentParser(description="Discover and OTA-flash WLED devices.")
|
||||||
|
sub = parser.add_subparsers(dest="cmd", required=True)
|
||||||
|
|
||||||
|
p_discover = sub.add_parser("discover", help="Scan network(s) for WLED devices.")
|
||||||
|
p_discover.add_argument("--subnet", action="append", help="Subnet CIDR, repeatable (example: 192.168.1.0/24).")
|
||||||
|
p_discover.add_argument("--timeout", type=float, default=0.8, help="HTTP probe timeout in seconds.")
|
||||||
|
p_discover.add_argument("--workers", type=int, default=128, help="Parallel probe workers.")
|
||||||
|
p_discover.add_argument("--output", default=DEFAULT_OUTPUT_FILE, help="Output file for discovered hosts.")
|
||||||
|
p_discover.set_defaults(func=cmd_discover)
|
||||||
|
|
||||||
|
p_flash = sub.add_parser("flash", help="Flash firmware.bin to selected hosts sequentially.")
|
||||||
|
p_flash.add_argument("--firmware", required=True, help="Path to firmware.bin")
|
||||||
|
p_flash.add_argument("--targets", help="Comma-separated IP list")
|
||||||
|
p_flash.add_argument("--targets-file", help="Text file with one IP per line")
|
||||||
|
p_flash.add_argument("--discover", action="store_true", help="Discover targets before flashing")
|
||||||
|
p_flash.add_argument("--subnet", action="append", help="Subnet CIDR for discovery mode")
|
||||||
|
p_flash.add_argument("--start-from", help="Start flashing from this IP within the resolved target list")
|
||||||
|
p_flash.add_argument("--timeout", type=float, default=0.8, help="HTTP probe timeout in seconds")
|
||||||
|
p_flash.add_argument("--workers", type=int, default=128, help="Parallel probe workers for discovery")
|
||||||
|
p_flash.add_argument("--connect-timeout", type=float, default=5.0, help="HTTP connect timeout in seconds")
|
||||||
|
p_flash.add_argument("--upload-timeout", type=float, default=90.0, help="HTTP upload timeout in seconds")
|
||||||
|
p_flash.add_argument("--reboot-wait", type=float, default=10.0, help="Sleep after upload before online check")
|
||||||
|
p_flash.add_argument("--offline-timeout", type=float, default=20.0, help="How long to wait for the device to disappear during reboot")
|
||||||
|
p_flash.add_argument("--online-timeout", type=float, default=60.0, help="How long to wait for device to come back")
|
||||||
|
p_flash.set_defaults(func=cmd_flash)
|
||||||
|
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = build_parser()
|
||||||
|
args = parser.parse_args()
|
||||||
|
try:
|
||||||
|
return int(args.func(args))
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\nAborted by user (Ctrl+C).")
|
||||||
|
print("Tip: rerun with --start-from <ip> to continue at a specific device.")
|
||||||
|
return 130
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
os.environ.setdefault("PYTHONUNBUFFERED", "1")
|
||||||
|
raise SystemExit(main())
|
||||||
159
tools/rfp_update_all_ota.py
Executable file
159
tools/rfp_update_all_ota.py
Executable file
@@ -0,0 +1,159 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Build and OTA-update the full RFP Infinity installation.
|
||||||
|
|
||||||
|
This script intentionally delegates flashing to tools/rfp_network_flash.py so
|
||||||
|
the actual OTA path stays the existing WLED /update workflow.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import ipaddress
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
NODE_ENV = "rfp_esp32s3_wroom1_n16r8_3x106"
|
||||||
|
MASTER_ENV = "rfp_esp32s3_wroom1_n16r8_master"
|
||||||
|
NODE_FIRMWARE = Path(".pio/build") / NODE_ENV / "firmware.bin"
|
||||||
|
MASTER_FIRMWARE = Path(".pio/build") / MASTER_ENV / "firmware.bin"
|
||||||
|
DEFAULT_SUBNET = "192.168.178.0/24"
|
||||||
|
NODE_HOSTS = range(11, 17)
|
||||||
|
MASTER_HOST = 10
|
||||||
|
|
||||||
|
|
||||||
|
def repo_root() -> Path:
|
||||||
|
return Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
|
||||||
|
def python_executable(root: Path) -> str:
|
||||||
|
local_python = root / ".venv/bin/python"
|
||||||
|
return str(local_python) if local_python.exists() else sys.executable
|
||||||
|
|
||||||
|
|
||||||
|
def platformio_env(root: Path) -> dict[str, str]:
|
||||||
|
env = os.environ.copy()
|
||||||
|
node_bin = "/home/jan/Documents/RFP/Finanz_App/node/current/bin"
|
||||||
|
env["PATH"] = f"{node_bin}:{env.get('PATH', '')}"
|
||||||
|
env["NPM_CONFIG_CACHE"] = str(root / ".npm-cache")
|
||||||
|
env["PLATFORMIO_CORE_DIR"] = str(root / ".piohome")
|
||||||
|
env["PLATFORMIO_PACKAGES_DIR"] = str(root / ".piohome/packages")
|
||||||
|
env["PLATFORMIO_PLATFORMS_DIR"] = str(root / ".piohome/platforms")
|
||||||
|
env["PLATFORMIO_CACHE_DIR"] = str(root / ".piohome/.cache")
|
||||||
|
env["PLATFORMIO_BUILD_CACHE_DIR"] = str(root / ".piohome/buildcache")
|
||||||
|
return env
|
||||||
|
|
||||||
|
|
||||||
|
def targets_from_subnet(subnet: str) -> tuple[list[str], str]:
|
||||||
|
network = ipaddress.ip_network(subnet, strict=False)
|
||||||
|
if network.version != 4:
|
||||||
|
raise ValueError("Only IPv4 subnets are supported.")
|
||||||
|
octets = str(network.network_address).split(".")
|
||||||
|
if len(octets) != 4:
|
||||||
|
raise ValueError(f"Invalid IPv4 subnet: {subnet}")
|
||||||
|
prefix = ".".join(octets[:3])
|
||||||
|
return [f"{prefix}.{host}" for host in NODE_HOSTS], f"{prefix}.{MASTER_HOST}"
|
||||||
|
|
||||||
|
|
||||||
|
def run(cmd: list[str], root: Path, env: dict[str, str], dry_run: bool) -> None:
|
||||||
|
print("+ " + " ".join(cmd))
|
||||||
|
if dry_run:
|
||||||
|
return
|
||||||
|
subprocess.run(cmd, cwd=root, env=env, check=True)
|
||||||
|
|
||||||
|
|
||||||
|
def build(root: Path, env_name: str, env: dict[str, str], dry_run: bool) -> None:
|
||||||
|
run([python_executable(root), "-m", "platformio", "run", "-e", env_name], root, env, dry_run)
|
||||||
|
|
||||||
|
|
||||||
|
def flash(root: Path, targets: list[str], firmware: Path, env: dict[str, str], dry_run: bool) -> None:
|
||||||
|
if not targets:
|
||||||
|
return
|
||||||
|
firmware_path = root / firmware
|
||||||
|
if not dry_run and not firmware_path.exists():
|
||||||
|
raise FileNotFoundError(f"Firmware does not exist: {firmware_path}")
|
||||||
|
run(
|
||||||
|
[
|
||||||
|
python_executable(root),
|
||||||
|
"tools/rfp_network_flash.py",
|
||||||
|
"flash",
|
||||||
|
"--targets",
|
||||||
|
",".join(targets),
|
||||||
|
"--firmware",
|
||||||
|
str(firmware),
|
||||||
|
],
|
||||||
|
root,
|
||||||
|
env,
|
||||||
|
dry_run,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def filtered_plan(nodes: list[str], master: str, args: argparse.Namespace) -> list[tuple[str, str]]:
|
||||||
|
plan: list[tuple[str, str]] = []
|
||||||
|
if not args.master_only:
|
||||||
|
plan.extend((ip, "node") for ip in nodes)
|
||||||
|
if not args.nodes_only:
|
||||||
|
plan.append((master, "master"))
|
||||||
|
|
||||||
|
if not args.start_from:
|
||||||
|
return plan
|
||||||
|
|
||||||
|
for index, (ip, _role) in enumerate(plan):
|
||||||
|
if ip == args.start_from:
|
||||||
|
return plan[index:]
|
||||||
|
raise ValueError(f"--start-from {args.start_from} is not in the selected update plan.")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args() -> argparse.Namespace:
|
||||||
|
parser = argparse.ArgumentParser(description="Build and OTA-update RFP Infinity nodes, then master.")
|
||||||
|
role = parser.add_mutually_exclusive_group()
|
||||||
|
role.add_argument("--nodes-only", action="store_true", help="Only build/flash nodes .11-.16")
|
||||||
|
role.add_argument("--master-only", action="store_true", help="Only build/flash master .10")
|
||||||
|
parser.add_argument("--start-from", help="Resume at this IP, for example 192.168.178.14 or 192.168.178.10")
|
||||||
|
parser.add_argument("--subnet", default=DEFAULT_SUBNET, help=f"Show subnet used to derive .10-.16 targets (default: {DEFAULT_SUBNET})")
|
||||||
|
parser.add_argument("--no-build", action="store_true", help="Skip PlatformIO builds and flash existing firmware.bin files")
|
||||||
|
parser.add_argument("--dry-run", action="store_true", help="Print build and flash steps without executing them")
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
args = parse_args()
|
||||||
|
root = repo_root()
|
||||||
|
env = platformio_env(root)
|
||||||
|
nodes, master = targets_from_subnet(args.subnet)
|
||||||
|
plan = filtered_plan(nodes, master, args)
|
||||||
|
node_targets = [ip for ip, role in plan if role == "node"]
|
||||||
|
master_targets = [ip for ip, role in plan if role == "master"]
|
||||||
|
|
||||||
|
if not plan:
|
||||||
|
print("Nothing selected.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
print("RFP Infinity OTA update plan:")
|
||||||
|
for ip, role in plan:
|
||||||
|
print(f"- {ip} ({role})")
|
||||||
|
print()
|
||||||
|
|
||||||
|
if not args.no_build:
|
||||||
|
if node_targets:
|
||||||
|
print(f"Building node firmware: {NODE_ENV}")
|
||||||
|
build(root, NODE_ENV, env, args.dry_run)
|
||||||
|
if master_targets:
|
||||||
|
print(f"Building master firmware: {MASTER_ENV}")
|
||||||
|
build(root, MASTER_ENV, env, args.dry_run)
|
||||||
|
|
||||||
|
if node_targets:
|
||||||
|
print("Flashing nodes sequentially...")
|
||||||
|
flash(root, node_targets, NODE_FIRMWARE, env, args.dry_run)
|
||||||
|
if master_targets:
|
||||||
|
print("Flashing master last...")
|
||||||
|
flash(root, master_targets, MASTER_FIRMWARE, env, args.dry_run)
|
||||||
|
|
||||||
|
print("OTA update plan completed.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
1634
wled00/infinity_sync.cpp
Normal file
1634
wled00/infinity_sync.cpp
Normal file
File diff suppressed because it is too large
Load Diff
@@ -168,6 +168,9 @@ String PinManagerClass::getPinSpecialText(int gpio) { // special purpose PIN in
|
|||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if defined(STATUSPIXELPIN)
|
||||||
|
if (gpio == STATUSPIXELPIN) return(F("WLED Status Pixel"));
|
||||||
|
#endif
|
||||||
#if defined(STATUSLED)
|
#if defined(STATUSLED)
|
||||||
if (gpio == STATUSLED) return(F("WLED Status LED"));
|
if (gpio == STATUSLED) return(F("WLED Status LED"));
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
171
wled00/wled.cpp
171
wled00/wled.cpp
@@ -71,6 +71,73 @@
|
|||||||
#endif
|
#endif
|
||||||
// WLEDMM end
|
// WLEDMM end
|
||||||
|
|
||||||
|
#if defined(STATUSLED) || defined(STATUSPIXELPIN)
|
||||||
|
static inline void writeStatusIndicator(uint32_t color) {
|
||||||
|
#if defined(STATUSPIXELPIN)
|
||||||
|
if (statusPixelBus != nullptr) {
|
||||||
|
if (statusPixelBus->canShow()) {
|
||||||
|
statusPixelBus->setPixelColor(0, color);
|
||||||
|
statusPixelBus->show();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if defined(STATUSLED)
|
||||||
|
#if STATUSLED >= 0
|
||||||
|
#ifdef STATUSLEDINVERTED
|
||||||
|
digitalWrite(STATUSLED, color ? LOW : HIGH);
|
||||||
|
#else
|
||||||
|
digitalWrite(STATUSLED, color ? HIGH : LOW);
|
||||||
|
#endif
|
||||||
|
#else
|
||||||
|
busses.setStatusPixel(color);
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if defined(STATUSPIXELPIN)
|
||||||
|
#ifndef STATUSPIXELCOLORORDER
|
||||||
|
#define STATUSPIXELCOLORORDER COL_ORDER_GRB
|
||||||
|
#endif
|
||||||
|
|
||||||
|
static void initStatusPixelBus() {
|
||||||
|
if (statusPixelBus != nullptr) return;
|
||||||
|
if (pinManager.isPinAllocated(STATUSPIXELPIN)) {
|
||||||
|
USER_PRINTF("Skipping status pixel on GPIO %u because the pin is already in use.\n", STATUSPIXELPIN);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32S2)
|
||||||
|
constexpr uint8_t maxStatusPixelBusses = 4;
|
||||||
|
#elif defined(CONFIG_IDF_TARGET_ESP32C3)
|
||||||
|
constexpr uint8_t maxStatusPixelBusses = 2;
|
||||||
|
#else
|
||||||
|
constexpr uint8_t maxStatusPixelBusses = 8;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
uint8_t busIndex = busses.getNumBusses();
|
||||||
|
if (busIndex >= maxStatusPixelBusses) {
|
||||||
|
USER_PRINTLN(F("Skipping status pixel because no free hardware LED channel is left."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t pins[] = {STATUSPIXELPIN};
|
||||||
|
BusConfig statusCfg(TYPE_WS2812_RGB, pins, 0, 1, STATUSPIXELCOLORORDER, false, 0, RGBW_MODE_MANUAL_ONLY);
|
||||||
|
statusPixelBus = new BusDigital(statusCfg, busIndex, busses.getColorOrderMap());
|
||||||
|
if (statusPixelBus == nullptr || !statusPixelBus->isOk()) {
|
||||||
|
delete statusPixelBus;
|
||||||
|
statusPixelBus = nullptr;
|
||||||
|
USER_PRINTLN(F("Failed to initialize onboard status pixel."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
statusPixelBus->setBrightness(255, true);
|
||||||
|
writeStatusIndicator(0);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
#if INCLUDE_xTaskGetHandle && defined(ARDUINO_ARCH_ESP32) && (defined(WLED_DEBUG) || defined(WLED_DEBUG_HEAP))
|
#if INCLUDE_xTaskGetHandle && defined(ARDUINO_ARCH_ESP32) && (defined(WLED_DEBUG) || defined(WLED_DEBUG_HEAP))
|
||||||
// WLEDMM stack debug tool - find async_tcp task, and queries it's free stack
|
// WLEDMM stack debug tool - find async_tcp task, and queries it's free stack
|
||||||
@@ -883,6 +950,9 @@ void WLED::setup()
|
|||||||
|
|
||||||
DEBUG_PRINTLN(F("Initializing strip"));
|
DEBUG_PRINTLN(F("Initializing strip"));
|
||||||
beginStrip();
|
beginStrip();
|
||||||
|
#if defined(STATUSPIXELPIN)
|
||||||
|
initStatusPixelBus();
|
||||||
|
#endif
|
||||||
DEBUG_PRINT(F("heap ")); DEBUG_PRINTLN(getFreeHeapSize());
|
DEBUG_PRINT(F("heap ")); DEBUG_PRINTLN(getFreeHeapSize());
|
||||||
|
|
||||||
USER_PRINTLN(F("\nUsermods setup ..."));
|
USER_PRINTLN(F("\nUsermods setup ..."));
|
||||||
@@ -1568,58 +1638,79 @@ void WLED::handleConnection()
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If status LED pin is allocated for other uses, does nothing
|
// If status LED pin is allocated for other uses, does nothing
|
||||||
// else blink at 1Hz when WLED_CONNECTED is false (no WiFi, ?? no Ethernet ??)
|
// green blink = DDP realtime active
|
||||||
// else blink at 2Hz when MQTT is enabled but not connected
|
// blue blink = AP active
|
||||||
// else turn the status LED off
|
// green solid = network connected
|
||||||
|
// amber fast blink = MQTT configured but disconnected
|
||||||
|
// red fast blink = WiFi configured but currently disconnected
|
||||||
|
// off = idle / no status to show
|
||||||
void WLED::handleStatusLED()
|
void WLED::handleStatusLED()
|
||||||
{
|
{
|
||||||
#if defined(STATUSLED)
|
#if defined(STATUSLED) || defined(STATUSPIXELPIN)
|
||||||
[[maybe_unused]] uint32_t c = 0;
|
uint32_t c = 0;
|
||||||
|
uint8_t nextType = 0;
|
||||||
|
uint16_t blinkIntervalMs = 0;
|
||||||
|
|
||||||
#if STATUSLED>=0
|
#if defined(STATUSLED) && STATUSLED>=0
|
||||||
if (pinManager.isPinAllocated(STATUSLED)) {
|
if (pinManager.isPinAllocated(STATUSLED)) {
|
||||||
return; //lower priority if something else uses the same pin
|
return; //lower priority if something else uses the same pin
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if (WLED_CONNECTED) {
|
if (realtimeMode == REALTIME_MODE_DDP) {
|
||||||
c = RGBW32(0,255,0,0);
|
c = RGBW32(0,255,0,0);
|
||||||
ledStatusType = 2;
|
nextType = 2;
|
||||||
} else if (WLED_MQTT_CONNECTED) {
|
blinkIntervalMs = 250;
|
||||||
c = RGBW32(0,128,0,0);
|
|
||||||
ledStatusType = 4;
|
|
||||||
} else if (apActive) {
|
} else if (apActive) {
|
||||||
c = RGBW32(0,0,255,0);
|
c = RGBW32(0,0,255,0);
|
||||||
ledStatusType = 1;
|
nextType = 2;
|
||||||
}
|
blinkIntervalMs = 500;
|
||||||
if (ledStatusType) {
|
} else if (WLED_CONNECTED) {
|
||||||
if (millis() - ledStatusLastMillis >= (1000/ledStatusType)) {
|
#ifndef WLED_DISABLE_MQTT
|
||||||
ledStatusLastMillis = millis();
|
if (mqttEnabled && mqttServer[0] != 0 && !WLED_MQTT_CONNECTED) {
|
||||||
#if 1
|
c = RGBW32(255,96,0,0);
|
||||||
// WLEDMM un-comment this to stop the blinking
|
nextType = 3;
|
||||||
if ((ledStatusType != 2) && (ledStatusType != 4))
|
blinkIntervalMs = 250;
|
||||||
ledStatusState = !ledStatusState;
|
} else
|
||||||
else
|
|
||||||
ledStatusState = HIGH;
|
|
||||||
#else
|
|
||||||
ledStatusState = !ledStatusState;
|
|
||||||
#endif
|
|
||||||
#if STATUSLED>=0
|
|
||||||
digitalWrite(STATUSLED, ledStatusState);
|
|
||||||
#else
|
|
||||||
busses.setStatusPixel(ledStatusState ? c : 0);
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
#if STATUSLED>=0
|
|
||||||
#ifdef STATUSLEDINVERTED
|
|
||||||
digitalWrite(STATUSLED, HIGH);
|
|
||||||
#else
|
|
||||||
digitalWrite(STATUSLED, LOW);
|
|
||||||
#endif
|
|
||||||
#else
|
|
||||||
busses.setStatusPixel(0);
|
|
||||||
#endif
|
#endif
|
||||||
|
{
|
||||||
|
c = RGBW32(0,255,0,0);
|
||||||
|
nextType = 1;
|
||||||
|
}
|
||||||
|
} else if (WLED_WIFI_CONFIGURED) {
|
||||||
|
c = RGBW32(255,0,0,0);
|
||||||
|
nextType = 3;
|
||||||
|
blinkIntervalMs = 250;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextType != ledStatusType) {
|
||||||
|
ledStatusType = nextType;
|
||||||
|
ledStatusLastMillis = millis();
|
||||||
|
ledStatusState = (nextType == 1);
|
||||||
|
writeStatusIndicator(ledStatusState ? c : 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextType == 0) {
|
||||||
|
if (ledStatusState) {
|
||||||
|
ledStatusState = false;
|
||||||
|
writeStatusIndicator(0);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextType == 1) {
|
||||||
|
if (!ledStatusState) {
|
||||||
|
ledStatusState = true;
|
||||||
|
writeStatusIndicator(c);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (millis() - ledStatusLastMillis >= blinkIntervalMs) {
|
||||||
|
ledStatusLastMillis = millis();
|
||||||
|
ledStatusState = !ledStatusState;
|
||||||
|
writeStatusIndicator(ledStatusState ? c : 0);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -751,12 +751,15 @@ WLED_GLOBAL bool doSerializeConfig _INIT(false); // flag to initiate savi
|
|||||||
WLED_GLOBAL bool doReboot _INIT(false); // flag to initiate reboot from async handlers
|
WLED_GLOBAL bool doReboot _INIT(false); // flag to initiate reboot from async handlers
|
||||||
WLED_GLOBAL bool doPublishMqtt _INIT(false);
|
WLED_GLOBAL bool doPublishMqtt _INIT(false);
|
||||||
|
|
||||||
// status led
|
// status led / status pixel
|
||||||
#if defined(STATUSLED)
|
#if defined(STATUSLED) || defined(STATUSPIXELPIN)
|
||||||
WLED_GLOBAL unsigned long ledStatusLastMillis _INIT(0);
|
WLED_GLOBAL unsigned long ledStatusLastMillis _INIT(0);
|
||||||
WLED_GLOBAL uint8_t ledStatusType _INIT(0); // current status type - corresponds to number of blinks per second
|
WLED_GLOBAL uint8_t ledStatusType _INIT(0); // 0=off, 1=solid, 2=slow blink, 3=fast blink
|
||||||
WLED_GLOBAL bool ledStatusState _INIT(false); // the current LED state
|
WLED_GLOBAL bool ledStatusState _INIT(false); // the current LED state
|
||||||
#endif
|
#endif
|
||||||
|
#if defined(STATUSPIXELPIN)
|
||||||
|
WLED_GLOBAL BusDigital* statusPixelBus _INIT(nullptr);
|
||||||
|
#endif
|
||||||
|
|
||||||
// server library objects
|
// server library objects
|
||||||
WLED_GLOBAL AsyncWebServer server _INIT_N(((80)));
|
WLED_GLOBAL AsyncWebServer server _INIT_N(((80)));
|
||||||
|
|||||||
Reference in New Issue
Block a user