mirror of
https://github.com/wled/WLED.git
synced 2026-06-23 13:22:18 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9672b9bee0 | |||
| 6d0921bdd7 | |||
| ea4661e907 |
@@ -1,259 +0,0 @@
|
||||
# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
|
||||
#
|
||||
# CodeRabbit configuration — references existing guideline files to avoid
|
||||
# duplicating conventions. See:
|
||||
# .github/copilot-instructions.md — project overview & general rules
|
||||
# docs/cpp.instructions.md — C++ coding conventions
|
||||
# docs/web.instructions.md — Web UI coding conventions
|
||||
# docs/cicd.instructions.md — GitHub Actions / CI-CD conventions
|
||||
# docs/hardening.instructions.md — basic rules for code hardening and robustness
|
||||
# docs/securecode.instructions.md — more detailed checklists for common vulnerabilities
|
||||
#
|
||||
# NOTE: This file must be committed (tracked by git) for CodeRabbit to read
|
||||
# it from the repository. If it is listed in .gitignore, CodeRabbit will
|
||||
# not see it and these settings will have no effect.
|
||||
|
||||
language: en-US
|
||||
|
||||
reviews:
|
||||
# generic review setting, see https://docs.coderabbit.ai/reference/configuration#reference
|
||||
auto_apply_labels: false
|
||||
# abort_on_close: false
|
||||
high_level_summary: true
|
||||
review_status: true
|
||||
collapse_walkthrough: false
|
||||
poem: false
|
||||
# sequence_diagrams: false
|
||||
auto_review:
|
||||
enabled: true
|
||||
base_branches:
|
||||
- main
|
||||
- 16_x
|
||||
- 0_15_x
|
||||
- V5
|
||||
ignore_title_keywords:
|
||||
- WIP
|
||||
tools:
|
||||
fbinfer:
|
||||
enabled: false # Arduino.h not available on Linux analysis host
|
||||
cppcheck:
|
||||
enabled: true # cppcheck works fine without Arduino headers
|
||||
clang:
|
||||
enabled: true # clang tidy likewise works
|
||||
|
||||
path_instructions:
|
||||
- path: "**/*.{cpp,h,hpp,ino}"
|
||||
instructions: >
|
||||
Follow the C++ coding conventions documented in docs/cpp.instructions.md
|
||||
and the general project guidelines in AGENTS.md and .github/copilot-instructions.md.
|
||||
|
||||
Key rules: 2-space indentation (no tabs), camelCase functions/variables,
|
||||
PascalCase classes, UPPER_CASE macros. No C++ exceptions — use return codes and debug macros.
|
||||
|
||||
Hot-path optimization guidelines (attributes, uint_fast types, caching,
|
||||
unsigned range checks) apply from pixel set/get operations and strip.show() downward —
|
||||
NOT to effect functions in FX.cpp, which have diverse contributor styles.
|
||||
|
||||
When reviewing PRs labeled "AI" or when source code appears to be AI-generated, perform these additional checks:
|
||||
1. VERIFY all referenced preprocessor macros, constants and flags exist by searching the codebase - do not trust the AI's claims about what exists.
|
||||
2. CHECK for reinvention: search for existing functions/patterns that already solve the same problem.
|
||||
3. CHECK for singleton data (defined but never used) and for dead/disabled code, and suggest to remove them.
|
||||
4. VERIFY comments match code behavior - AI frequently generates plausible but incorrect comments.
|
||||
5. VERIFY numerical stability / accuracy of arithmetic expressions. AI is often wrong when it comes to math and numbers.
|
||||
6. CHECK for implied but weakly justified assumptions - like usermod loop() call frequency - and ask for clarification.
|
||||
7. FLAG changes that appear unrelated: deleted comments, unnecessary re-formatting or re-factoring, and modifications in files that seem unrelated to the PR description.
|
||||
|
||||
# ── Security hardening — firmware (trust-boundary-aware) ────────────────
|
||||
- path: "wled00/**/*.{cpp,h,hpp,ino}"
|
||||
instructions: >
|
||||
Apply the WLED security hardening rules from docs/hardening.instructions.md,
|
||||
and consult docs/securecode.instructions.md when more details are needed for actionable recommendations.
|
||||
|
||||
Trust Boundary Model — enforce input-validation and bounds-checking rules
|
||||
ONLY at the first untrusted ingress point. Untrusted ingress points are:
|
||||
- HTTP/JSON API request bodies and query parameters (/json/*, /win, etc.)
|
||||
- WebSocket message payloads
|
||||
- UDP datagrams (parsePacket() / recvfrom() and protocol wrappers for
|
||||
E1.31, DDP, Art-Net, TPM2.net)
|
||||
- TCP socket reads
|
||||
- Serial/UART command input
|
||||
- ESP-NOW raw messages input
|
||||
|
||||
A value that has been validated and range-clamped at its ingress handler is
|
||||
considered TRUSTED for all subsequent WLED core processing. Do NOT flag or suggest
|
||||
repeated bounds/range checks or internal uses of already-sanitized data.
|
||||
When it is unclear whether a value has been sanitized upstream, prefer
|
||||
requesting clarification over raising a false-positive finding.
|
||||
|
||||
- path: "wled00/data/**"
|
||||
instructions: >
|
||||
Follow the web UI conventions documented in docs/web.instructions.md.
|
||||
|
||||
Key rules: indent HTML and JavaScript with tabs, CSS with tabs.
|
||||
Files here are built into wled00/html_*.h and wled00/js_*.h by tools/cdata.js — never
|
||||
edit those generated headers directly.
|
||||
|
||||
# ── Security hardening — WebUI (always an ingress/output surface) ────────
|
||||
- path: "wled00/data/**"
|
||||
instructions: >
|
||||
Apply the WLED web UI security rules from docs/securecode.instructions.md
|
||||
(sections WEB1-WEB7).
|
||||
|
||||
The Trust Boundary Model does NOT reduce scope here: the WebUI is both
|
||||
an ingress point (user input, postMessage, fetched config data) and an
|
||||
output/rendering surface. Always flag DOM XSS risks, unsafe
|
||||
innerHTML / document.write / insertAdjacentHTML / outerHTML assignments,
|
||||
postMessage handlers without origin validation, eval() / new Function(),
|
||||
unsafe location.href or location.replace() assignments, and DOM insertion
|
||||
from fetched or config-derived data — regardless of where the data
|
||||
originates.
|
||||
|
||||
- path: "wled00/html_*.h"
|
||||
instructions: >
|
||||
These files are auto-generated from wled00/data/ by tools/cdata.js.
|
||||
They must never be manually edited or committed. Flag any PR that
|
||||
includes changes to these files.
|
||||
|
||||
- path: "wled00/js_*.h"
|
||||
instructions: >
|
||||
These files are auto-generated from wled00/data/ by tools/cdata.js.
|
||||
They must never be manually edited or committed. Flag any PR that
|
||||
includes changes to these files.
|
||||
|
||||
- path: "usermods/**"
|
||||
instructions: >
|
||||
Usermods are community add-ons.
|
||||
Each usermod lives in its own directory under usermods/ and is implemented
|
||||
as a .cpp file with a dedicated library.json file to manage dependencies.
|
||||
Follow the same C++ conventions as the core firmware (docs/cpp.instructions.md).
|
||||
|
||||
# ── Security hardening — usermods (trust-boundary-aware, narrow scope) ───
|
||||
- path: "usermods/**/*.{cpp,h,hpp}"
|
||||
instructions: >
|
||||
For usermods, the untrusted ingress points are:
|
||||
- readFromConfig(JsonObject& root) and calls to getJsonValue()
|
||||
- readFromJsonState(JsonObject& obj) — JSON is parsed, but values are client-supplied
|
||||
- onMqttMessage(char* topic, char* payload) — raw network strings, no core sanitization
|
||||
- onEspNowMessage(uint8_t* sender, uint8_t* payload, uint8_t len) — raw radio bytes
|
||||
- onUdpPacket(uint8_t* payload, size_t len) — raw UDP buffer, no core filtering
|
||||
Values retrieved at these ingress points are considered trusted only after the
|
||||
usermod itself has validated and range-clamped them.
|
||||
|
||||
Flag ONLY downstream uses of ingress-derived values where an out-of-range or
|
||||
unexpected value can cause misbehaviour that is not already guarded, for example:
|
||||
- `switch` statements on an ingress-derived value with no `default` branch,
|
||||
or with a missing `break` where fall-through is unintentional
|
||||
- array or buffer indexing with an ingress-derived value where the index is
|
||||
not clamped before use
|
||||
- arithmetic with an ingress-derived value that can overflow or produce a
|
||||
negative result used as a size or count
|
||||
|
||||
Do NOT flag:
|
||||
- getJsonValue() call sites themselves (type coercion is handled by ArduinoJson)
|
||||
- Internal logic that operates on values already confirmed safe at ingress
|
||||
- Repeated range checks on values that have already been clamped
|
||||
- General memory-safety patterns unrelated to ingress-derived data flow
|
||||
|
||||
- path: ".github/workflows/*.{yml,yaml}"
|
||||
instructions: >
|
||||
Follow the CI/CD conventions documented in docs/cicd.instructions.md.
|
||||
|
||||
Key rules: 2-space indentation, descriptive name: on every workflow/job/step.
|
||||
Third-party actions must be pinned to a specific version tag — branch pins
|
||||
such as @main or @master are not allowed. Declare explicit permissions: blocks
|
||||
scoped to least privilege. Never interpolate github.event.* values directly
|
||||
into run: steps — pass them through an env: variable to prevent script
|
||||
injection. Do not use pull_request_target unless fully justified.
|
||||
|
||||
- path: "**/*.instructions.md"
|
||||
instructions: |
|
||||
This file contains both AI-facing rules and human-only reference sections.
|
||||
Human-only sections are enclosed in `<!-- HUMAN_ONLY_START -->` /
|
||||
`<!-- HUMAN_ONLY_END -->` HTML comment markers and should not be used as
|
||||
actionable review criteria.
|
||||
|
||||
When this file is modified in a PR, perform the following alignment check:
|
||||
1. For each `<!-- HUMAN_ONLY_START --> ... <!-- HUMAN_ONLY_END -->` block,
|
||||
verify that its examples and guidance are consistent with (and do not
|
||||
contradict) the AI-facing rules stated in the same file.
|
||||
2. Flag any HUMAN_ONLY section whose content has drifted from the surrounding
|
||||
AI-facing rules due to edits introduced in this PR.
|
||||
3. If new AI-facing rules were added without updating a related HUMAN_ONLY
|
||||
reference section, note this as a suggestion (not a required fix).
|
||||
|
||||
# ── Secrets / sensitive information scanning ────────────────────────────
|
||||
- path: "platformio*.ini*"
|
||||
instructions: >
|
||||
Scan for secrets, passwords, and other sensitive information accidentally
|
||||
committed to PlatformIO configuration files (platformio.ini,
|
||||
platformio_override.ini, platformio_override.ini.sample).
|
||||
|
||||
Flag any of the following:
|
||||
- build_flags entries that define credentials as literal values, e.g.:
|
||||
-DWIFI_SSID=\"<YOUR_SSID>\" -DWIFI_PASS=\"<YOUR_PASSWORD>\"
|
||||
-DOTA_PASS=\"<OTA_PASSWORD>\" -DMQTT_PASS=\"<MQTT_PASSWORD>\"
|
||||
Flag only when the value is not a recognisable placeholder (see below).
|
||||
- upload_flags or upload_port values that embed a password or auth token (e.g., --auth=<PASSWORD> or any URL using credential-bearing userinfo).
|
||||
- Any key = <value> pair whose key name contains "pass", "password",
|
||||
"secret", "token", "key", "credential", or "auth" where the value is
|
||||
a non-empty, non-placeholder literal string.
|
||||
- Hardcoded IP addresses or hostnames paired with credentials in the
|
||||
same environment section.
|
||||
- API keys or access tokens as literal strings in any field.
|
||||
|
||||
Do NOT flag:
|
||||
- Values that are clearly template placeholders (e.g., YOUR_SSID,
|
||||
<YOUR_PASSWORD>, changeme, example_token, your_password_here).
|
||||
- Values that use PlatformIO environment variable substitution (${sysenv.WIFI_PASS} or ${env:WIFI_PASS}).
|
||||
- Comments that only explain what a field should contain.
|
||||
- platformio_override.ini.sample entries that contain only
|
||||
placeholder/example values.
|
||||
|
||||
- path: "usermods/**/library.json"
|
||||
instructions: >
|
||||
Scan for secrets and sensitive information in usermod dependency manifests.
|
||||
|
||||
Flag any of the following:
|
||||
- Dependency URLs that embed credentials in the URL itself (e.g., any URL containing credential-bearing userinfo).
|
||||
- Personal access tokens, OAuth tokens, or API keys as literal strings
|
||||
anywhere in the file.
|
||||
- Values matching well-known secret patterns: GitHub PATs (ghp_...,
|
||||
github_pat_...), AWS access keys (AKIA...), or similarly structured
|
||||
high-entropy tokens.
|
||||
|
||||
Do NOT flag:
|
||||
- Plain HTTPS or SSH URLs without embedded credentials.
|
||||
- Version specifiers, semver ranges, or commit SHA references that
|
||||
contain no credential prefix.
|
||||
- Repository owner/name path segments (not credential material).
|
||||
|
||||
- path: "usermods/**/{readme,README,Readme}.md"
|
||||
instructions: >
|
||||
Scan for secrets, passwords, and sensitive information in usermod
|
||||
documentation files, including inside code blocks, inline code, and prose.
|
||||
|
||||
Flag any of the following:
|
||||
- Hardcoded Wi-Fi SSID or password values that appear to be real (non-placeholder)
|
||||
strings in configuration or installation examples.
|
||||
- Hardcoded OTA, AP, or MQTT passwords in code snippets or step-by-step
|
||||
instructions.
|
||||
- API keys, bearer tokens, or access tokens shown as literal values.
|
||||
- Example platformio_override.ini snippets that contain real-looking
|
||||
credential values instead of placeholders.
|
||||
- Hardcoded IP addresses combined with credentials in the same example.
|
||||
|
||||
Do NOT flag:
|
||||
- Values that are clearly template placeholders (e.g., YOUR_SSID,
|
||||
<password>, my_secret, changeme, ****).
|
||||
- Generic prose describing what a field means without supplying a value.
|
||||
- Asterisk-masked values (e.g., ******, ••••••).
|
||||
|
||||
finishing_touches:
|
||||
# Docstrings | Options for generating Docstrings for your PRs/MRs.
|
||||
docstrings:
|
||||
# Docstrings | Allow CodeRabbit to generate docstrings for PRs/MRs.
|
||||
# default: true - disabled in WLED: has caused confusion in the past
|
||||
enabled: false
|
||||
unit_tests:
|
||||
# default: true - disabled in WLED: we don't have a unit test framework, this option just confuses contributors
|
||||
enabled: false
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"features": {
|
||||
"ghcr.io/ar90n/devcontainer-features/platformio:1": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "ghcr.io/ar90n/devcontainer-features/platformio@sha256:4a28f8c147d81ff996afebe6f43e453355b6373dd4022960351090b532ac9dd3",
|
||||
"integrity": "sha256:4a28f8c147d81ff996afebe6f43e453355b6373dd4022960351090b532ac9dd3"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/node:2": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "ghcr.io/devcontainers/features/node@sha256:fedd4c11f7adfb64283b578dddc7da906728daa25fa293351c9d913231acf12f",
|
||||
"integrity": "sha256:fedd4c11f7adfb64283b578dddc7da906728daa25fa293351c9d913231acf12f"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
{
|
||||
"name": "WLED-dev",
|
||||
"image": "mcr.microsoft.com/devcontainers/python:3",
|
||||
"features": {
|
||||
"ghcr.io/ar90n/devcontainer-features/platformio:1": {},
|
||||
"ghcr.io/devcontainers/features/node:2": {
|
||||
"version": "v20.18.3"
|
||||
"name": "Python 3",
|
||||
"build": {
|
||||
"dockerfile": "Dockerfile",
|
||||
"context": "..",
|
||||
"args": {
|
||||
// Update 'VARIANT' to pick a Python version: 3, 3.6, 3.7, 3.8, 3.9
|
||||
"VARIANT": "3"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -42,8 +43,7 @@
|
||||
},
|
||||
"extensions": [
|
||||
"ms-python.python",
|
||||
"platformio.platformio-ide",
|
||||
"ms-vscode.cpptools-extension-pack"
|
||||
"platformio.platformio-ide"
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -52,7 +52,7 @@
|
||||
// "forwardPorts": [],
|
||||
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
"postCreateCommand": "",
|
||||
"postCreateCommand": "bash -i -c 'nvm install && npm ci'",
|
||||
|
||||
// Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
|
||||
"remoteUser": "vscode"
|
||||
|
||||
+2
-1
@@ -1,2 +1,3 @@
|
||||
github: [DedeHai,lost-hope,willmmiles,netmindz,softhack007]
|
||||
github: [Aircoookie,blazoncek,DedeHai,lost-hope,willmmiles]
|
||||
custom: ['https://paypal.me/Aircoookie','https://paypal.me/blazoncek']
|
||||
thanks_dev: u/gh/netmindz
|
||||
|
||||
@@ -5,9 +5,7 @@ body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Please quickly search existing issues first before submitting a bug.
|
||||
Please don't submit long error analyses created by an AI agent, these are often totally wrong.
|
||||
Just describe the problem in your own words.
|
||||
Please quickly search existing issues first before submitting a bug.
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
@@ -46,10 +44,7 @@ body:
|
||||
id: version
|
||||
attributes:
|
||||
label: What version of WLED?
|
||||
description: |-
|
||||
Find this by going to <kbd><samp>⚙️ Config</samp></kbd> → <kbd><samp>Security & Updates</samp></kbd> → Scroll to Bottom.
|
||||
Copy and paste the rest of the line that begins “<samp>Installed version: </samp>”,
|
||||
or, for older versions, the entire line after “<samp>Server message</samp>”.
|
||||
description: You can find this in by going to Config -> Security & Updates -> Scroll to Bottom. Copy and paste the entire line after "Server message"
|
||||
placeholder: "e.g. WLED 0.13.1 (build 2203150)"
|
||||
validations:
|
||||
required: true
|
||||
@@ -61,13 +56,10 @@ body:
|
||||
options:
|
||||
- ESP8266
|
||||
- ESP32
|
||||
- ESP32 with ethernet
|
||||
- ESP32-S2
|
||||
- ESP32-S3
|
||||
- ESP32-S2
|
||||
- ESP32-C3
|
||||
- ESP32-C5 (experimental)
|
||||
- ESP32-C6 (experimental)
|
||||
- ESP32-P4 (experimental)
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
---
|
||||
applyTo: "**"
|
||||
---
|
||||
# Agent-Mode Build & Test Instructions
|
||||
|
||||
Detailed build workflow, timeouts, and troubleshooting for making code changes in agent mode. Always reference these instructions first when running builds or validating changes.
|
||||
|
||||
## Build Timing and Timeouts
|
||||
|
||||
Use these timeout values when running builds:
|
||||
|
||||
| Command | Typical Time | Minimum Timeout | Notes |
|
||||
|---|---|---|---|
|
||||
| `npm run build` | ~3 s | 30 s | Web UI → `wled00/html_*.h` `wled00/js_*.h` headers |
|
||||
| `npm test` | ~40 s | 2 min | Validates build system |
|
||||
| `npm run dev` | continuous | — | Watch mode, auto-rebuilds on changes |
|
||||
| `pio run -e <env>` | 15–20 min | 30 min | First build downloads toolchains; subsequent builds are faster |
|
||||
|
||||
**NEVER cancel long-running builds.** PlatformIO downloads and compilation require patience.
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Code Style Summary
|
||||
- **C++** files in `wled00/` and `usermods/`: 2-space indentation (no tabs), camelCase functions/variables, PascalCase classes, UPPER_CASE macros. No C++ exceptions — use return codes and debug macros.
|
||||
- **Web UI** files in `wled00/data`: indent HTML and JavaScript with tabs, CSS with tabs.
|
||||
- **CI/CD workflows** in `.github/workflows`: 2-space indentation, descriptive `name:` on every workflow/job/step. Third-party actions must be pinned to a specific version tag — branch pins such as `@main` or `@master` are not allowed. SHA pinning recommended.
|
||||
|
||||
### Web UI Changes
|
||||
|
||||
1. Edit files in `wled00/data/`
|
||||
2. Run `npm run build` to regenerate `wled00/html_*.h` `wled00/js_*.h` headers
|
||||
3. Test with local HTTP server (see Manual Testing below)
|
||||
4. Run `npm test` to validate
|
||||
|
||||
### Firmware Changes
|
||||
|
||||
1. Edit files in `wled00/` (but **never** `html_*.h` and `js_*.h` files)
|
||||
2. Ensure web UI is built first: `npm run build`
|
||||
3. Build firmware: `pio run -e esp32dev` (set timeout ≥ 30 min)
|
||||
4. Flash to device: `pio run -e [target] --target upload`
|
||||
|
||||
### Combined Web + Firmware Changes
|
||||
|
||||
1. Always build web UI first
|
||||
2. Test web interface manually
|
||||
3. Then build and test firmware
|
||||
|
||||
|
||||
## Before Finishing Work - Testing
|
||||
|
||||
**You MUST complete ALL of these before marking work as done:**
|
||||
|
||||
1. **Run tests**: `npm test` — must pass
|
||||
2. **Build firmware**: `pio run -e esp32dev` — must succeed after source code changes, **never skip this step**.
|
||||
- Set timeout to 30+ minutes, **never cancel**
|
||||
- Choose `esp32dev` as a common, representative environment
|
||||
- If the build fails, fix the issue before proceeding
|
||||
3. **For web UI changes**: manually test the interface (see below)
|
||||
|
||||
If any step fails, fix the issue. **Do NOT mark work complete with failing builds or tests.**
|
||||
|
||||
## Manual Web UI Testing
|
||||
|
||||
Start a local server:
|
||||
|
||||
```sh
|
||||
cd wled00/data && python3 -m http.server 8080
|
||||
# Open http://localhost:8080/index.htm
|
||||
```
|
||||
|
||||
Test these scenarios after every web UI change:
|
||||
|
||||
- **Load**: `index.htm` loads without JavaScript errors (check browser console)
|
||||
- **Navigation**: switching between main page and settings pages works
|
||||
- **Color controls**: color picker and brightness controls function correctly
|
||||
- **Effects**: effect selection and parameter changes work
|
||||
- **Settings**: form submission and validation work
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Build Issues
|
||||
|
||||
| Problem | Solution |
|
||||
|---|---|
|
||||
| Missing `html_*.h` | Run `npm ci; npm run build` |
|
||||
| Web UI looks broken | Check browser console for JS errors |
|
||||
| PlatformIO network errors | Retry — downloads can be flaky |
|
||||
| Node.js version mismatch | Ensure Node.js 20+ (check `.nvmrc`) |
|
||||
|
||||
### Recovery Steps
|
||||
|
||||
- **Force web UI rebuild**: `npm run build -- -f`
|
||||
- **Clear generated files**: `rm -f wled00/html_*.h wled00/js_*.h` then `npm run build`
|
||||
- **Clean PlatformIO build artifacts**: `pio run --target clean`
|
||||
- **Reinstall Node deps**: `rm -rf node_modules && npm ci`
|
||||
|
||||
## CI/CD Validation
|
||||
|
||||
The GitHub Actions CI workflow will:
|
||||
1. Install Node.js and Python dependencies
|
||||
2. Run `npm test`
|
||||
3. Build web UI (automatic via PlatformIO)
|
||||
4. Compile firmware for **all** `default_envs` targets
|
||||
|
||||
**To ensure CI success, always validate locally:**
|
||||
- Run `npm test` and ensure it passes
|
||||
- Run `pio run -e esp32dev` (or another common firmware environment, see next section) and ensure it completes successfully
|
||||
- If either fails locally, it WILL fail in CI
|
||||
|
||||
Match this workflow in local development to catch failures before pushing.
|
||||
|
||||
## Important Reminders
|
||||
|
||||
- Always **commit source code**
|
||||
- Every pull request MUST include a clear description of *what* changed and *why*.
|
||||
- **Never edit or commit** `wled00/html_*.h` and `wled00/js_*.h` — auto-generated from `wled00/data/`
|
||||
- After modifying source code files, check that any **previous comments have been preserved** or updated to reflect the new behaviour.
|
||||
- Web UI rebuild is part of the PlatformIO firmware compilation pipeline
|
||||
- Common environments: `nodemcuv2`, `esp32dev`, `esp8266_2m`, `esp32c3dev`, `esp32s3dev_8MB_opi`
|
||||
- List all PlatformIO targets: `pio run --list-targets`
|
||||
+111
-101
@@ -4,125 +4,135 @@ WLED is a fast and feature-rich implementation of an ESP32 and ESP8266 webserver
|
||||
|
||||
Always reference these instructions first and fallback to search or bash commands only when you encounter unexpected information that does not match the info here.
|
||||
|
||||
> **Note for AI review tools**: sections enclosed in
|
||||
> `<!-- HUMAN_ONLY_START -->` / `<!-- HUMAN_ONLY_END -->` HTML comments contain
|
||||
> contributor reference material. Do **not** use that content as actionable review
|
||||
> criteria — treat it as background context only.
|
||||
## Working Effectively
|
||||
|
||||
## Setup
|
||||
### Initial Setup
|
||||
- Install Node.js 20+ (specified in `.nvmrc`): Check your version with `node --version`
|
||||
- Install dependencies: `npm ci` (takes ~5 seconds)
|
||||
- Install PlatformIO for hardware builds: `pip install -r requirements.txt` (takes ~60 seconds)
|
||||
|
||||
- Node.js 20+ (see `.nvmrc`)
|
||||
- Install dependencies: `npm ci`
|
||||
- PlatformIO (required only for firmware compilation): `pip install -r requirements.txt`
|
||||
### Build and Test Workflow
|
||||
- **ALWAYS build web UI first**: `npm run build` -- takes 3 seconds. NEVER CANCEL.
|
||||
- **Run tests**: `npm test` -- takes 40 seconds. NEVER CANCEL. Set timeout to 2+ minutes.
|
||||
- **Development mode**: `npm run dev` -- monitors file changes and auto-rebuilds web UI
|
||||
- **Hardware firmware build**: `pio run -e [environment]` -- takes 15+ minutes. NEVER CANCEL. Set timeout to 30+ minutes.
|
||||
|
||||
## Build and Test
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
### Build Process Details
|
||||
The build has two main phases:
|
||||
1. **Web UI Generation** (`npm run build`):
|
||||
- Processes files in `wled00/data/` (HTML, CSS, JS)
|
||||
- Minifies and compresses web content
|
||||
- Generates `wled00/html_*.h` files with embedded web content
|
||||
- **CRITICAL**: Must be done before any hardware build
|
||||
|
||||
| Command | Purpose | Typical Time |
|
||||
|---|---|---|
|
||||
| `npm run build` | Build web UI → generates `wled00/html_*.h` and `wled00/js_*.h` headers | ~3 s |
|
||||
| `npm test` | Run test suite | ~40 s |
|
||||
| `npm run dev` | Watch mode — auto-rebuilds web UI on file changes | — |
|
||||
| `pio run -e <env>` | Build firmware for a hardware target | 15–20 min |
|
||||
2. **Hardware Compilation** (`pio run`):
|
||||
- Compiles C++ firmware for various ESP32/ESP8266 targets
|
||||
- Common environments: `nodemcuv2`, `esp32dev`, `esp8266_2m`
|
||||
- List all targets: `pio run --list-targets`
|
||||
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
## Validation and Testing
|
||||
|
||||
- **Always run `npm run build` before any `pio run`** (and run `npm ci` first on fresh clones or when lockfile/dependencies change).
|
||||
- The web UI build generates required `wled00/html_*.h` and `wled00/js_*.h` headers for firmware compilation.
|
||||
- **Build firmware to validate code changes**: `pio run -e esp32dev` — must succeed, never skip this step.
|
||||
- Common firmware environments: `nodemcuv2`, `esp32dev`, `esp8266_2m`, `esp32c3dev`, `esp32s3dev_8MB_opi`
|
||||
### Web UI Testing
|
||||
- **ALWAYS validate web UI changes manually**:
|
||||
- Start local server: `cd wled00/data && python3 -m http.server 8080`
|
||||
- Open `http://localhost:8080/index.htm` in browser
|
||||
- Test basic functionality: color picker, effects, settings pages
|
||||
- **Check for JavaScript errors** in browser console
|
||||
|
||||
For detailed build timeouts, development workflows, troubleshooting, and validation steps, see [agent-build.instructions.md](agent-build.instructions.md).
|
||||
### Code Validation
|
||||
- **No automated linting configured** - follow existing code style in files you edit
|
||||
- **Code style**: Use tabs for web files (.html/.css/.js), spaces (2 per level) for C++ files
|
||||
- **C++ formatting available**: `clang-format` is installed but not in CI
|
||||
- **Always run tests before finishing**: `npm test`
|
||||
|
||||
### Usermod Guidelines
|
||||
### Manual Testing Scenarios
|
||||
After making changes to web UI, always test:
|
||||
- **Load main interface**: Verify index.htm loads without errors
|
||||
- **Navigation**: Test switching between main page and settings pages
|
||||
- **Color controls**: Verify color picker and brightness controls work
|
||||
- **Effects**: Test effect selection and parameter changes
|
||||
- **Settings**: Test form submission and validation
|
||||
|
||||
- New custom effects can be added into the user_fx usermod. Read the [user_fx documentation](https://github.com/wled/WLED/blob/main/usermods/user_fx/README.md) for guidance.
|
||||
- Other usermods may be based on the [EXAMPLE usermod](https://github.com/wled/WLED/tree/main/usermods/EXAMPLE). Never edit the example, always create a copy!
|
||||
- New usermod IDs can be added into [wled00/const.h](https://github.com/wled/WLED/blob/main/wled00/const.h#L160).
|
||||
- To activate a usermod, a custom build configuration should be used. Add the usermod name to `custom_usermods`.
|
||||
|
||||
## Project Structure Overview
|
||||
|
||||
### Project Branch / Release Structure
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
|
||||
```text
|
||||
main # Main development trunk (daily/nightly) 17.0.0-dev
|
||||
├── V5 # special branch: code rework for esp-idf 5.5.x (unstable)
|
||||
├── V5-C6 # special branch: integration of new MCU types: esp32-c5, esp32-c6, esp32-p4 (unstable)
|
||||
16_x # maintenance for release 16.x.y
|
||||
0_15_x # maintenance (bugfixes only) for current release 0.15.4
|
||||
(tag) v0.14.4 # previous version 0.14.4 (no maintenance)
|
||||
(tag) v0.13.3 # old version 0.13.3 (no maintenance)
|
||||
(tag) v0. ... . ... # historical versions 0.12.x and before
|
||||
```
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
|
||||
- ``main``: development trunk (daily/nightly)
|
||||
- ``V5`` and ``V5-C6``: code rework for esp-idf 5.5.x (unstable) - branched from ``main``.
|
||||
- ``16_x``: maintenance for release 16.x.y
|
||||
- ``0_15_x``: bugfixing / maintenance for release 0.15.x
|
||||
## Common Tasks
|
||||
|
||||
### Repository Structure
|
||||
|
||||
tl;dr:
|
||||
* Firmware source: `wled00/` (C++). Web UI source: `wled00/data/`. Build targets: `platformio.ini`.
|
||||
* Auto-generated headers: `wled00/html_*.h` and `wled00/js_*.h` — **never edit or commit**.
|
||||
* ArduinoJSON + AsyncJSON: `wled00/src/dependencies/json` (included via `wled.h`). CI/CD: `.github/workflows/`.
|
||||
* Usermods: `usermods/` (C++, with individual library.json).
|
||||
* Contributor docs: `docs/` (coding guidelines, etc).
|
||||
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
Detailed overview:
|
||||
|
||||
```text
|
||||
wled00/ # Main firmware source (C++) "WLED core"
|
||||
├── data/ # Web interface files
|
||||
│ ├── index.htm # Main UI
|
||||
```
|
||||
wled00/ # Main firmware source (C++)
|
||||
├── data/ # Web interface files
|
||||
│ ├── index.htm # Main UI
|
||||
│ ├── settings*.htm # Settings pages
|
||||
│ └── *.js/*.css # Frontend resources
|
||||
├── *.cpp/*.h # Firmware source files
|
||||
├── html_*.h # Auto-generated embedded web files (DO NOT EDIT, DO NOT COMMIT)
|
||||
├── src/ # Modules used by the WLED core (C++)
|
||||
│ ├── fonts/ # Font libraries for scrolling text effect
|
||||
└ └── dependencies/ # Utility functions - some of them have their own licensing terms
|
||||
lib/ # Project specific custom libraries. PlatformIO will compile them to separate static libraries and link them
|
||||
platformio.ini # Hardware build configuration
|
||||
|
||||
platformio_override.sample.ini # examples for custom build configurations - entries must be copied into platformio_override.ini to use them.
|
||||
# platformio_override.ini is _not_ stored in the WLED repository!
|
||||
usermods/ # User-contributed addons to the WLED core, maintained by individual contributors (C++, with individual library.json)
|
||||
package.json # Node.js dependencies and scripts, release identification
|
||||
pio-scripts/ # Build tools (PlatformIO)
|
||||
tools/ # Build tools (Node.js), partition files, and generic utilities
|
||||
│ └── *.js/*.css # Frontend resources
|
||||
├── *.cpp/*.h # Firmware source files
|
||||
└── html_*.h # Generated embedded web files (DO NOT EDIT)
|
||||
tools/ # Build tools (Node.js)
|
||||
├── cdata.js # Web UI build script
|
||||
└── cdata-test.js # Test suite
|
||||
docs/ # Contributor docs, coding guidelines
|
||||
platformio.ini # Hardware build configuration
|
||||
package.json # Node.js dependencies and scripts
|
||||
.github/workflows/ # CI/CD pipelines
|
||||
```
|
||||
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
## General Guidelines
|
||||
### Key Files and Their Purpose
|
||||
- `wled00/data/index.htm` - Main web interface
|
||||
- `wled00/data/settings*.htm` - Configuration pages
|
||||
- `tools/cdata.js` - Converts web files to C++ headers
|
||||
- `wled00/wled.h` - Main firmware configuration
|
||||
- `platformio.ini` - Hardware build targets and settings
|
||||
|
||||
- **Repository language is English.** Suggest translations for non-English content.
|
||||
- **Use VS Code with PlatformIO extension** for best development experience.
|
||||
- **Never edit or commit** `wled00/html_*.h` and `wled00/js_*.h` — auto-generated from `wled00/data/`.
|
||||
- If updating Web UI files in `wled00/data/`, **make use of common functions in `wled00/data/common.js` whenever possible**.
|
||||
- **When unsure, say so.** Gather more information rather than guessing.
|
||||
- **Acknowledge good patterns** when you see them. Positive feedback always helps.
|
||||
- **Provide references** when making analyses or recommendations. Base them on the correct branch or PR.
|
||||
- **Highlight user-visible breaking changes and ripple effects**. Ask for confirmation that these were introduced intentionally.
|
||||
- **Unused / dead code must be justified or removed**. This helps to keep the codebase clean, maintainable and readable.
|
||||
- **Verify feature-flag names.** Every `WLED_ENABLE_*` / `WLED_DISABLE_*` flag must exactly match one of the names below — misspellings are silently ignored by the preprocessor (e.g. `WLED_IR_DISABLE` instead of `WLED_DISABLE_INFRARED`), causing silent build variations. Flag unrecognised names as likely typos and suggest the correct spelling.
|
||||
<br>**`WLED_DISABLE_*`**: `2D`, `ADALIGHT`, `ALEXA`, `BROWNOUT_DET`, `ESPNOW`, `FILESYSTEM`, `HUESYNC`, `IMPROV_WIFISCAN`, `INFRARED`, `LOXONE`, `MQTT`, `OTA`, `PARTICLESYSTEM1D`, `PARTICLESYSTEM2D`, `PIXELFORGE`, `WEBSOCKETS`
|
||||
<br>**`WLED_ENABLE_*`**: `ADALIGHT`, `AOTA`, `DMX`, `DMX_INPUT`, `DMX_OUTPUT`, `FS_EDITOR`, `GIF`, `HUB75MATRIX`, `JSONLIVE`, `LOXONE`, `MQTT`, `PIXART`, `PXMAGIC`, `USERMOD_PAGE`, `WEBSOCKETS`, `WPA_ENTERPRISE`
|
||||
- **C++ formatting available**: `clang-format` is installed but not in CI
|
||||
- No automated linting is configured — match existing code style in files you edit.
|
||||
### Development Workflow
|
||||
1. **For web UI changes**:
|
||||
- Edit files in `wled00/data/`
|
||||
- Run `npm run build` to regenerate headers
|
||||
- Test with local HTTP server
|
||||
- Run `npm test` to validate build system
|
||||
|
||||
Refer to `docs/cpp.instructions.md` and `docs/web.instructions.md` for language-specific conventions, and `docs/cicd.instructions.md` for GitHub Actions workflows.
|
||||
2. **For firmware changes**:
|
||||
- Edit files in `wled00/` (but NOT `html_*.h` files)
|
||||
- Ensure web UI is built first (`npm run build`)
|
||||
- Build firmware: `pio run -e [target]`
|
||||
- Flash to device: `pio run -e [target] --target upload`
|
||||
|
||||
### Pull Request Expectations
|
||||
3. **For both web and firmware**:
|
||||
- Always build web UI first
|
||||
- Test web interface manually
|
||||
- Build and test firmware if making firmware changes
|
||||
|
||||
- **No force-push on open PRs.** Once a pull request is open and being reviewed, do not force-push (`git push --force`) to the branch. Force-pushing rewrites history that reviewers may have already commented on, making it impossible to track incremental changes. Use regular commits or `git merge` to incorporate feedback; the branch will be squash-merged when it is accepted.
|
||||
- **Modifications to ``platformio.ini`` MUST be approved explicitly** by a *maintainer* or *WLED organisation Member*. Modifications to the global build environment may break github action builds. Always flag them.
|
||||
- **Document your changes in the PR.** Every pull request should include a clear description of *what* changed and *why*. If the change affects user-visible behavior, describe the expected impact. Link to related issues where applicable. Provide screenshots to showcase new features.
|
||||
## Build Timing and Timeouts
|
||||
|
||||
- **Web UI build**: 3 seconds - Set timeout to 30 seconds minimum
|
||||
- **Test suite**: 40 seconds - Set timeout to 2 minutes minimum
|
||||
- **Hardware builds**: 15+ minutes - Set timeout to 30+ minutes minimum
|
||||
- **NEVER CANCEL long-running builds** - PlatformIO downloads and compilation can take significant time
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
- **Build fails with missing html_*.h**: Run `npm run build` first
|
||||
- **Web UI looks broken**: Check browser console for JavaScript errors
|
||||
- **PlatformIO network errors**: Try again, downloads can be flaky
|
||||
- **Node.js version issues**: Ensure Node.js 20+ is installed (check `.nvmrc`)
|
||||
|
||||
### When Things Go Wrong
|
||||
- **Clear generated files**: `rm -f wled00/html_*.h` then rebuild
|
||||
- **Force web UI rebuild**: `npm run build -- --force` or `npm run build -- -f`
|
||||
- **Clean PlatformIO cache**: `pio run --target clean`
|
||||
- **Reinstall dependencies**: `rm -rf node_modules && npm install`
|
||||
|
||||
## Important Notes
|
||||
|
||||
- **DO NOT edit `wled00/html_*.h` files** - they are auto-generated
|
||||
- **Always commit both source files AND generated html_*.h files**
|
||||
- **Web UI must be built before firmware compilation**
|
||||
- **Test web interface manually after any web UI changes**
|
||||
- **Use VS Code with PlatformIO extension for best development experience**
|
||||
- **Hardware builds require appropriate ESP32/ESP8266 development board**
|
||||
|
||||
## CI/CD Pipeline
|
||||
The GitHub Actions workflow:
|
||||
1. Installs Node.js and Python dependencies
|
||||
2. Runs `npm test` to validate build system
|
||||
3. Builds web UI with `npm run build`
|
||||
4. Compiles firmware for multiple hardware targets
|
||||
5. Uploads build artifacts
|
||||
|
||||
Match this workflow in your local development to ensure CI success.
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
; ----------------------------------------------------------------------------
|
||||
; platformio_release.ini.template
|
||||
; ----------------------------------------------------------------------------
|
||||
; Copied to platformio_release.ini by the release CI workflow
|
||||
; (.github/workflows/release.yml -> build.yml with `release: true`)
|
||||
; in order to extend the matrix of `default_envs` built and published
|
||||
; for tagged releases and remove the debugging usersmods env
|
||||
;
|
||||
; This file overrides `[platformio].default_envs` from platformio.ini via
|
||||
; `extra_configs`. It MUST list every env that should be released - including
|
||||
; the regular CI default_envs - because it fully replaces the parent value.
|
||||
;
|
||||
; Do NOT commit a generated platformio_release.ini (it's in .gitignore).
|
||||
; ----------------------------------------------------------------------------
|
||||
|
||||
[platformio]
|
||||
default_envs = nodemcuv2
|
||||
esp8266_2m
|
||||
esp8266_2m_min
|
||||
esp01_1m_full
|
||||
nodemcuv2_160
|
||||
esp8266_2m_160
|
||||
esp01_1m_full_160
|
||||
nodemcuv2_compat
|
||||
esp8266_2m_compat
|
||||
esp01_1m_full_compat
|
||||
esp32dev
|
||||
esp32dev_debug
|
||||
esp32_eth
|
||||
esp32_wrover
|
||||
lolin_s2_mini
|
||||
esp32c3dev
|
||||
esp32c3dev_qio
|
||||
esp32S3_wroom2
|
||||
esp32s3dev_16MB_opi
|
||||
esp32s3dev_8MB_opi
|
||||
esp32s3dev_8MB_qspi
|
||||
esp32s3dev_8MB_none
|
||||
esp32s3_4M_qspi
|
||||
; HUB75 release-only envs
|
||||
esp32dev_hub75
|
||||
esp32dev_hub75_forum_pinout
|
||||
adafruit_matrixportal_esp32s3
|
||||
esp32s3dev_16MB_opi_hub75 ;; MoonHub
|
||||
esp32s3dev_4MB_qspi_hub75 ;; HD-WF2
|
||||
@@ -3,15 +3,7 @@ name: WLED Build
|
||||
# Only included into other workflows
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
release:
|
||||
description: 'Build the release env matrix (uses .github/platformio_release.ini.template)'
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
|
||||
jobs:
|
||||
|
||||
get_default_envs:
|
||||
@@ -19,9 +11,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Apply release config
|
||||
if: inputs.release
|
||||
run: cp .github/platformio_release.ini.template platformio_release.ini
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
@@ -46,9 +35,6 @@ jobs:
|
||||
environment: ${{ fromJSON(needs.get_default_envs.outputs.environments) }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Apply release config
|
||||
if: inputs.release
|
||||
run: cp .github/platformio_release.ini.template platformio_release.ini
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
@@ -65,8 +51,8 @@ jobs:
|
||||
~/.platformio/.cache
|
||||
~/.buildcache
|
||||
build_output
|
||||
key: pio-${{ runner.os }}-${{ matrix.environment }}-${{ hashFiles('platformio.ini', '.github/platformio_release.ini.template', 'pio-scripts/output_bins.py') }}-${{ hashFiles('wled00/**', 'usermods/**') }}
|
||||
restore-keys: pio-${{ runner.os }}-${{ matrix.environment }}-${{ hashFiles('platformio.ini', '.github/platformio_release.ini.template', 'pio-scripts/output_bins.py') }}-
|
||||
key: pio-${{ runner.os }}-${{ matrix.environment }}-${{ hashFiles('platformio.ini', 'pio-scripts/output_bins.py') }}-${{ hashFiles('wled00/**', 'usermods/**') }}
|
||||
restore-keys: pio-${{ runner.os }}-${{ matrix.environment }}-${{ hashFiles('platformio.ini', 'pio-scripts/output_bins.py') }}-
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
@@ -77,21 +63,9 @@ jobs:
|
||||
|
||||
- name: Build firmware
|
||||
run: pio run -e ${{ matrix.environment }}
|
||||
- name: Get artifact name from bin filename
|
||||
id: artifact_name
|
||||
run: |
|
||||
bin=$(ls build_output/release/*.bin 2>/dev/null | head -1)
|
||||
if [ -n "$bin" ]; then
|
||||
# Strip WLED_<version>_ prefix from WLED_<version>_<RELEASE_NAME>.bin
|
||||
base=$(basename "$bin" .bin)
|
||||
release_name=$(echo "$base" | sed 's/^[^_]*_[^_]*_//')
|
||||
echo "name=firmware-$release_name" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "name=firmware-${{ matrix.environment }}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ steps.artifact_name.outputs.name }}
|
||||
name: firmware-${{ matrix.environment }}
|
||||
path: |
|
||||
build_output/release/*.bin
|
||||
build_output/release/*_ESP02*.bin.gz
|
||||
|
||||
@@ -7,9 +7,6 @@ on:
|
||||
# This can be used to allow manually triggering nightlies from the web interface
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
jobs:
|
||||
wled_build:
|
||||
uses: ./.github/workflows/build.yml
|
||||
@@ -26,22 +23,21 @@ jobs:
|
||||
run: ls -la
|
||||
- name: "✏️ Generate release changelog"
|
||||
id: changelog
|
||||
uses: janheinrichmerker/action-github-changelog-generator@v2.4
|
||||
uses: janheinrichmerker/action-github-changelog-generator@v2.3
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
sinceTag: v16.0.0
|
||||
output: CHANGELOG_NIGHTLY.md
|
||||
sinceTag: v0.15.0
|
||||
# Exclude issues that were closed without resolution from changelog
|
||||
excludeLabels: 'stale,wontfix,duplicate,invalid,external,question,use-as-is,not_planned'
|
||||
exclude-labels: 'stale,wontfix,duplicate,invalid'
|
||||
- name: Update Nightly Release
|
||||
uses: andelf/nightly-release@5834076edc55cc05975561c9722043f072ac5c26
|
||||
uses: andelf/nightly-release@main
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: nightly
|
||||
name: 'Nightly Release $$'
|
||||
prerelease: true
|
||||
body_path: CHANGELOG_NIGHTLY.md
|
||||
body: ${{ steps.changelog.outputs.changelog }}
|
||||
files: |
|
||||
*.bin
|
||||
*.bin.gz
|
||||
|
||||
@@ -33,6 +33,6 @@
|
||||
run: |
|
||||
jq -n \
|
||||
--arg content "Pull Request #${PR_NUMBER} \"${PR_TITLE}\" merged by ${ACTOR}
|
||||
${PR_URL} . It will be included in the next nightly builds, please test" \
|
||||
${PR_URL}. It will be included in the next nightly builds, please test" \
|
||||
'{content: $content}' \
|
||||
| curl -H "Content-Type: application/json" -d @- ${{ secrets.DISCORD_WEBHOOK_BETA_TESTERS }}
|
||||
|
||||
@@ -5,15 +5,10 @@ on:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
jobs:
|
||||
|
||||
wled_build:
|
||||
uses: ./.github/workflows/build.yml
|
||||
with:
|
||||
release: true
|
||||
|
||||
release:
|
||||
name: Create Release
|
||||
@@ -23,26 +18,21 @@ jobs:
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
merge-multiple: true
|
||||
- name: Create draft release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
body: "Release assets uploaded. Release notes pending..."
|
||||
draft: True
|
||||
files: |
|
||||
*.bin
|
||||
*.bin.gz
|
||||
- name: "✏️ Generate release changelog"
|
||||
id: changelog
|
||||
uses: janheinrichmerker/action-github-changelog-generator@v2.4
|
||||
uses: janheinrichmerker/action-github-changelog-generator@v2.3
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
sinceTag: v0.15.0
|
||||
maxIssues: 500
|
||||
# Exclude issues that were closed without resolution from changelog
|
||||
excludeLabels: 'stale,wontfix,duplicate,invalid,external,question,use-as-is,not_planned'
|
||||
- name: Update release description
|
||||
exclude-labels: 'stale,wontfix,duplicate,invalid'
|
||||
- name: Create draft release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
body: ${{ steps.changelog.outputs.changelog }}
|
||||
draft: True
|
||||
files: |
|
||||
*.bin
|
||||
*.bin.gz
|
||||
|
||||
|
||||
+13
-112
@@ -1,54 +1,36 @@
|
||||
name: Usermod CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- usermods/**
|
||||
push:
|
||||
paths:
|
||||
- usermods/**
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
- .github/workflows/usermods.yml
|
||||
pull_request:
|
||||
paths:
|
||||
- usermods/**
|
||||
|
||||
jobs:
|
||||
|
||||
get_usermod_envs:
|
||||
# Only run for pull requests from forks (not from branches within wled/WLED)
|
||||
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository
|
||||
name: Gather Usermods
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Get changed usermod environments
|
||||
python-version: '3.12'
|
||||
cache: 'pip'
|
||||
- name: Install PlatformIO
|
||||
run: pip install -r requirements.txt
|
||||
- name: Get default environments
|
||||
id: envs
|
||||
run: |
|
||||
# Usermods whose directories changed in this PR
|
||||
changed=$(git diff --name-only ${{ github.event.pull_request.base.sha }} HEAD \
|
||||
| grep '^usermods/' | cut -d/ -f2 | sort -u || true)
|
||||
|
||||
# All usermods with a library.json (excluding known-incompatible ones)
|
||||
all=$(find usermods/ -name library.json \
|
||||
| xargs dirname | xargs -n 1 basename \
|
||||
| grep -v PWM_fan | grep -v BME68X_v2 | grep -v pixels_dice_tray \
|
||||
| sort || true)
|
||||
|
||||
if [ -z "$changed" ] || [ -z "$all" ]; then
|
||||
echo "usermods=[]" >> $GITHUB_OUTPUT
|
||||
else
|
||||
usermods=$(comm -12 <(echo "$all") <(echo "$changed") | jq -R | jq --slurp -c)
|
||||
echo "usermods=$usermods" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
echo "usermods=$(find usermods/ -name library.json | xargs dirname | xargs -n 1 basename | jq -R | grep -v PWM_fan | grep -v BME68X_v2| grep -v pixels_dice_tray | jq --slurp -c)" >> $GITHUB_OUTPUT
|
||||
outputs:
|
||||
usermods: ${{ steps.envs.outputs.usermods }}
|
||||
|
||||
|
||||
build:
|
||||
# Only run for pull requests from forks (not from branches within wled/WLED)
|
||||
# Skip when no changed usermods were found (e.g. only non-library changes)
|
||||
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository && needs.get_usermod_envs.outputs.usermods != '[]'
|
||||
name: Build Enviornments
|
||||
runs-on: ubuntu-latest
|
||||
needs: get_usermod_envs
|
||||
@@ -89,85 +71,4 @@ jobs:
|
||||
cat platformio_override.ini
|
||||
|
||||
- name: Build firmware
|
||||
run: pio run -e ${{ matrix.environment }}
|
||||
|
||||
|
||||
get_custom_build_envs:
|
||||
name: Gather Custom Build Environments
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Find usermods with custom build environments
|
||||
id: custom_envs
|
||||
run: |
|
||||
# On PRs: only scan usermods whose directories changed.
|
||||
# On push: scan all usermods (validates the full set on merge).
|
||||
if [ "${{ github.event_name }}" = "pull_request" ]; then
|
||||
changed=$(git diff --name-only ${{ github.event.pull_request.base.sha }} HEAD \
|
||||
| grep '^usermods/' | cut -d/ -f2 | sort -u || true)
|
||||
if [ -z "$changed" ]; then
|
||||
echo "matrix=[]" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
samples=$(for mod in $changed; do
|
||||
f="usermods/$mod/platformio_override.ini.sample"
|
||||
[ -f "$f" ] && echo "$f"
|
||||
done | sort)
|
||||
else
|
||||
samples=$(find usermods/ -name "platformio_override.ini.sample" | sort)
|
||||
fi
|
||||
|
||||
result='[]'
|
||||
for sample in $samples; do
|
||||
usermod=$(dirname "$sample" | xargs basename)
|
||||
# Skip usermods known to be incompatible (same list as get_usermod_envs)
|
||||
case "$usermod" in PWM_fan|BME68X_v2|pixels_dice_tray) continue ;; esac
|
||||
envs=$(grep -E '^\[env:[^]]+\]' "$sample" | sed 's/^\[env:\(.*\)\]$/\1/')
|
||||
for env in $envs; do
|
||||
result=$(echo "$result" | jq --arg u "$usermod" --arg e "$env" '. + [{usermod: $u, env: $e}]')
|
||||
done
|
||||
done
|
||||
echo "matrix=$(echo "$result" | jq -c '.')" >> $GITHUB_OUTPUT
|
||||
outputs:
|
||||
matrix: ${{ steps.custom_envs.outputs.matrix }}
|
||||
|
||||
|
||||
build_custom:
|
||||
name: Build Custom Env (${{ matrix.usermod }} / ${{ matrix.env }})
|
||||
runs-on: ubuntu-latest
|
||||
needs: get_custom_build_envs
|
||||
if: needs.get_custom_build_envs.outputs.matrix != '[]'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include: ${{ fromJSON(needs.get_custom_build_envs.outputs.matrix) }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
- run: npm ci
|
||||
- name: Cache PlatformIO
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.platformio/.cache
|
||||
~/.buildcache
|
||||
build_output
|
||||
key: pio-${{ runner.os }}-${{ matrix.env }}-${{ hashFiles('platformio.ini', 'pio-scripts/output_bins.py') }}-${{ hashFiles('wled00/**', 'usermods/**') }}
|
||||
restore-keys: pio-${{ runner.os }}-${{ matrix.env }}-${{ hashFiles('platformio.ini', 'pio-scripts/output_bins.py') }}-
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
cache: 'pip'
|
||||
- name: Install PlatformIO
|
||||
run: pip install -r requirements.txt
|
||||
- name: Apply custom build environment
|
||||
run: cp -v "usermods/${{ matrix.usermod }}/platformio_override.ini.sample" platformio_override.ini
|
||||
- name: Build firmware
|
||||
run: pio run -e ${{ matrix.env }}
|
||||
run: pio run -e ${{ matrix.environment }}
|
||||
|
||||
+1
-8
@@ -7,16 +7,9 @@
|
||||
.pioenvs
|
||||
.piolibdeps
|
||||
.vscode
|
||||
compile_commands.json
|
||||
__pycache__/
|
||||
|
||||
/.dummy
|
||||
/dependencies.lock
|
||||
/managed_components
|
||||
|
||||
esp01-update.sh
|
||||
platformio_override.ini
|
||||
platformio_release.ini
|
||||
replace_fs.py
|
||||
wled-update.sh
|
||||
|
||||
@@ -30,4 +23,4 @@ wled-update.sh
|
||||
/wled00/Release
|
||||
/wled00/wled00.ino.cpp
|
||||
/wled00/html_*.h
|
||||
/wled00/js_*.h
|
||||
compile_commands.json
|
||||
|
||||
@@ -1,267 +0,0 @@
|
||||
# AGENTS.md — WLED AI Coding Agent & AI Code Review Reference
|
||||
|
||||
WLED is C++ firmware for ESP32/ESP8266 microcontrollers controlling addressable LEDs,
|
||||
with a web UI (HTML/JS/CSS). Built with PlatformIO (Arduino framework) and Node.js tooling.
|
||||
|
||||
See also: `.github/copilot-instructions.md`, `.github/agent-build.instructions.md`,
|
||||
`docs/cpp.instructions.md`, `docs/web.instructions.md`, `docs/cicd.instructions.md`,
|
||||
`docs/hardening.instructions.md`, `docs/securecode.instructions.md`.
|
||||
|
||||
Always reference these instructions - including coding guidelines in `docs/` - first and fallback to search or bash commands only when you encounter unexpected information that does not match the info here.
|
||||
|
||||
## Build Commands
|
||||
|
||||
| Command | Purpose | Timeout |
|
||||
|---|---|---|
|
||||
| `npm ci` | Install Node.js deps (required first) | 30s |
|
||||
| `npm run build` | Build web UI into `wled00/html_*.h` / `wled00/js_*.h` | 30s |
|
||||
| `npm test` | Run test suite (Node.js built-in `node --test`) | 2 min |
|
||||
| `npm run dev` | Watch mode — auto-rebuilds web UI on changes | continuous |
|
||||
| `pio run -e esp32dev` | Build firmware (ESP32, most common target) | 5 min |
|
||||
| `pio run -e nodemcuv2` | Build firmware (ESP8266) | 5 min |
|
||||
|
||||
**Always run `npm ci && npm run build` before `pio run`.** The web UI build generates
|
||||
required C headers for firmware compilation.
|
||||
|
||||
### Running a Single Test
|
||||
|
||||
Tests use Node.js built-in test runner (`node:test`). The single test file is
|
||||
`tools/cdata-test.js`. Run it with:
|
||||
|
||||
```bash
|
||||
npm test # runs all tests via `node --test`
|
||||
node --test tools/cdata-test.js # run just that file directly
|
||||
```
|
||||
|
||||
There are no C++ unit tests. Firmware is validated by successful compilation across
|
||||
target environments. Always build after code changes: `pio run -e esp32dev`.
|
||||
|
||||
### Common Firmware Environments
|
||||
|
||||
`esp32dev`, `nodemcuv2`, `esp8266_2m`, `esp32c3dev`, `esp32s3dev_8MB_opi`, `lolin_s2_mini`
|
||||
|
||||
### Recovery / Troubleshooting
|
||||
|
||||
```bash
|
||||
npm run build -- -f # force web UI rebuild
|
||||
rm -f wled00/html_*.h wled00/js_*.h && npm run build # clean + rebuild UI
|
||||
pio run --target clean # clean PlatformIO build artifacts
|
||||
rm -rf node_modules && npm ci # reinstall Node.js deps
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```text
|
||||
wled00/ # Main firmware source (C++)
|
||||
data/ # Web UI source (HTML/JS/CSS) — tabs for indentation
|
||||
html_*.h, js_*.h # Auto-generated (NEVER edit or commit)
|
||||
src/ # Sub-modules: fonts, bundled dependencies (ArduinoJSON)
|
||||
usermods/ # Community usermods (each has library.json + .cpp/.h)
|
||||
platformio.ini # Build configuration and environments
|
||||
pio-scripts/ # PlatformIO build scripts (Python)
|
||||
tools/ # Node.js build tools (cdata.js) and tests
|
||||
docs/ # Coding convention docs
|
||||
.github/workflows/ # CI/CD (GitHub Actions)
|
||||
```
|
||||
|
||||
### Branch / Release Structure
|
||||
|
||||
```text
|
||||
main # Main development trunk (daily/nightly) 17.0.0-dev. Target branch for PRs.
|
||||
├── V5 # special branch: code rework for esp-idf 5.5.x (unstable)
|
||||
├── V5-C6 # special branch: integration of new MCU types: esp32-c5, esp32-c6, esp32-p4 (unstable)
|
||||
16_x # maintenance for release 16.0.x
|
||||
0_15_x # maintenance (bugfixes only) for previous release 0.15.x
|
||||
(tag) v0.14.4 # old version 0.14.4 (no maintenance)
|
||||
(tag) v0.13.3 # old version 0.13.3 (no maintenance)
|
||||
(tag) v0. ... . ... # historical versions 0.12.x and before
|
||||
```
|
||||
|
||||
## C++ Code Style (wled00/, usermods/)
|
||||
|
||||
### Formatting
|
||||
- **2-space indentation** (no tabs in C++ files)
|
||||
- K&R brace style preferred (opening brace on same line)
|
||||
- Single-statement `if` bodies may omit braces: `if (a == b) doStuff(a);`
|
||||
- Space after keywords (`if (...)`, `for (...)`), no space before function parens (`doStuff(a)`)
|
||||
- No enforced line-length limit
|
||||
|
||||
### Comments
|
||||
- `//` for inline (always space after), `/* */` for block comments
|
||||
- Important: AI-generated source code blocks **must be mark with `// AI: below section was generated by an AI` / `// AI: end`**
|
||||
|
||||
### Naming Conventions
|
||||
| Kind | Convention | Examples |
|
||||
|---|---|---|
|
||||
| Functions, variables | camelCase | `setRandomColor()`, `effectCurrent` |
|
||||
| Classes, structs | PascalCase | `BusConfig`, `UsermodTemperature` |
|
||||
| Macros, constants | UPPER_CASE | `WLED_MAX_USERMODS`, `FX_MODE_STATIC` |
|
||||
| Private members | _camelCase | `_type`, `_bri`, `_len` |
|
||||
| Enum values | PascalCase | `PinOwner::BusDigital` |
|
||||
|
||||
### Includes
|
||||
- Include `"wled.h"` as the primary project header
|
||||
- Project headers first, then platform/Arduino, then third-party
|
||||
- Platform-conditional includes wrapped in `#ifdef ARDUINO_ARCH_ESP32` / `#ifdef ESP8266`
|
||||
|
||||
### Types and Const
|
||||
- Prefer `const &` for read-only function parameters
|
||||
- Mark getter/query methods `const`; use `static` for methods not accessing instance state
|
||||
- Prefer `constexpr` over `#define` for compile-time constants when possible
|
||||
- Use `static_assert` over `#if ... #error`
|
||||
- Use `uint_fast16_t` / `uint_fast8_t` in hot-path code
|
||||
|
||||
### Error Handling
|
||||
- **No C++ exceptions** — some builds disable them
|
||||
- Use return codes (`false`, `-1`) and global flags (`errorFlag = ERR_LOW_MEM`)
|
||||
- Use early returns as guard clauses: `if (!enabled || (strip.isUpdating() && (millis() - last_time < MAX_USERMOD_DELAY))) return;`
|
||||
- Debug output: `DEBUG_PRINTF()` / `DEBUG_PRINTLN()` (compiled out unless `-D WLED_DEBUG`)
|
||||
|
||||
### Strings and Memory
|
||||
- Use `F("string")` for string constants (saves RAM on ESP8266)
|
||||
- Use `PSTR()` with `DEBUG_PRINTF_P()` for format strings
|
||||
- Avoid `String` in hot paths; acceptable in config/setup code
|
||||
- Use `d_malloc()` (DRAM-preferred) / `p_malloc()` (PSRAM-preferred) for allocation
|
||||
- No VLAs — use fixed arrays or heap allocation
|
||||
- Call `reserve()` on strings/vectors to pre-allocate and avoid fragmentation
|
||||
|
||||
#### ESP32 PSRAM guidelines
|
||||
|
||||
- **Check availability**: Test chip availability with `psramFound() && ESP.getPsramSize() > 0` before assuming PSRAM is present. Never rely on `BOARD_HAS_PSRAM`only.
|
||||
- **DMA compatibility**: on ESP32 (classic), PSRAM buffers are **not DMA-capable**. On ESP32-S3 with octal PSRAM (`CONFIG_SPIRAM_MODE_OCT`), PSRAM buffers *can* be used with DMA when `CONFIG_SOC_PSRAM_DMA_CAPABLE` is defined.
|
||||
- **Fragmentation**: PSRAM allocations fragment less than DRAM because the region is larger. But avoid mixing small and large allocations in PSRAM — small allocations waste the MMU page granularity.
|
||||
- **Performance**: Prefer DRAM (or IRAM) for hot-path data that is *frequently* used. Prefer PSRAM for capacity-oriented buffers where slightly slower access times can be tolerated.
|
||||
|
||||
Background Info:
|
||||
|
||||
- PSRAM access is up to 15× slower than DRAM on ESP32 (dual-SPI bus), 3–10× slower than DRAM on ESP32-S3/-S2 with quad-SPI bus. On ESP32-S3 with octal PSRAM (`CONFIG_SPIRAM_MODE_OCT`), the penalty is smaller (~2×) because the 8-line DTR bus can transfer 8 bits in parallel. On ESP32-P4 with hex PSRAM (`CONFIG_SPIRAM_MODE_HEX`), the 16-line bus runs at 200 MHz which brings it on-par with DRAM.
|
||||
- Consider that ESP32 often crashes when the largest available DRAM chunk gets below 10 KB.
|
||||
|
||||
### Preprocessor / Feature Flags
|
||||
- Feature toggling: `WLED_DISABLE_*` and `WLED_ENABLE_*` flags (exact names matter!)
|
||||
- `WLED_DISABLE_*`: `2D`, `ADALIGHT`, `ALEXA`, `MQTT`, `OTA`, `INFRARED`, `WEBSOCKETS`, etc.
|
||||
- `WLED_ENABLE_*`: `DMX`, `GIF`, `HUB75MATRIX`, `JSONLIVE`, `WEBSOCKETS`, etc.
|
||||
- Platform: `ARDUINO_ARCH_ESP32`, `ESP8266`, `CONFIG_IDF_TARGET_ESP32S3`
|
||||
|
||||
### Math Functions
|
||||
- Use `sin8_t()`, `cos8_t()` — NOT `sin8()`, `cos8()` (removed, won't compile)
|
||||
- Use `sin_approx()` / `cos_approx()` instead of `sinf()` / `cosf()`
|
||||
- Replace `inoise8` / `inoise16` with `perlin8` / `perlin16`
|
||||
|
||||
### Hot-Path Code (Pixel Pipeline)
|
||||
- Use function attributes: `IRAM_ATTR`, `WLED_O2_ATTR`, `__attribute__((hot))`
|
||||
- Cache class members to locals before loops
|
||||
- Pre-compute invariants outside loops; use reciprocals to avoid division
|
||||
- Unsigned range checks: `if ((uint_fast16_t)(pix - start) < len)`
|
||||
|
||||
### ESP32 Tasks
|
||||
- `delay(1)` in custom FreeRTOS tasks (NOT `yield()`) — feeds IDLE watchdog
|
||||
- Do not use `delay()` in effects (FX.cpp) or hot pixel path
|
||||
|
||||
#### ESP32 Task Synchronization
|
||||
|
||||
- Use FreeRTOS mutexes, semaphores or queues when true concurrent access from multiple FreeRTOS tasks is possible, and race-conditions can lead to unexpected behaviour.
|
||||
- **Avoid `portENTER_CRITICAL()` / `portEXIT_CRITICAL()`**, as these functions stall the complete system and may cause LEDs flickering. Prefer FreeRTOS mutexes, semaphores or queues.
|
||||
- **Important**: Not every shared resource needs a mutex. Some synchronization is guaranteed by the overall control flow, for example when function calls are sequenced within the same loop iteration.
|
||||
- Consider using `std::atomic` or RAII scoped guards as alternatives to mutexes, semaphores or queues.
|
||||
|
||||
## Web UI Code Style (wled00/data/)
|
||||
|
||||
- **Tab indentation** for HTML, JS, and CSS
|
||||
- camelCase for JS functions/variables
|
||||
- Reuse helpers from `common.js` — do not duplicate utilities
|
||||
- After editing, run `npm run build` to regenerate headers
|
||||
- **Never edit** `wled00/html_*.h` or `wled00/js_*.h` directly
|
||||
|
||||
## Usermod Pattern
|
||||
|
||||
Usermods live in `usermods/<name>/` with a `.cpp`, optional `.h`, `library.json`, and `readme.md`.
|
||||
|
||||
```cpp
|
||||
class MyUsermod : public Usermod {
|
||||
private:
|
||||
bool enabled = false;
|
||||
static const char _name[];
|
||||
public:
|
||||
void setup() override { /* ... */ } // runs once at start-up
|
||||
void loop() override { /* ... */ } // runs once per main loop iteration
|
||||
void addToConfig(JsonObject& root) override { /* ... */ } // create/add persistent settings (usermod settings)
|
||||
bool readFromConfig(JsonObject& root) override { /* ... */ } // read from persistent settings (usermod settings UI)
|
||||
uint16_t getId() override { return USERMOD_ID_MYMOD; }
|
||||
void addToJsonInfo(JsonObject& root) override { /* ... */ } // Add custom items to the "info" page and to /json/info
|
||||
void appendConfigData() override { /* ... */ } // Customize the settings page: dropdowns, checkboxes, extra text, etc. Buffer size is limited!
|
||||
};
|
||||
const char MyUsermod::_name[] PROGMEM = "MyUsermod";
|
||||
static MyUsermod myUsermod;
|
||||
REGISTER_USERMOD(myUsermod);
|
||||
```
|
||||
|
||||
refer to detailed examples in `usermods/EXAMPLE/`, `usermods/user_fx/` and [in the user documentation for custom features](https://kno.wled.ge/advanced/custom-features/).
|
||||
|
||||
- Activate via `custom_usermods = ` in platformio build config. The `usermod_v2_` prefix or `_v2` suffix can be omitted.
|
||||
- Base new usermods on `usermods/EXAMPLE/` (never edit the example directly)
|
||||
- Store repeated strings as `static const char[] PROGMEM`
|
||||
- Add usermod IDs to `wled00/const.h` **only when a unique ID is required** (see below)
|
||||
|
||||
### Usermod IDs
|
||||
|
||||
A unique ID (registered in `wled00/const.h` and overriding `getId()`) is **only required** when a usermod needs one or more of the following:
|
||||
|
||||
1. **Inter-usermod communication** — another usermod or an FX effect calls `UsermodManager::lookup(mod_id)` or `UsermodManager::getUMData(..., mod_id)` to find or request data from this specific usermod.
|
||||
2. **Pin ownership via `pinManager`** — the usermod allocates GPIO pins through `pinManager`. Pin ownership is tracked by `PinOwner` enum values that map directly to `USERMOD_ID_*` constants (see `wled00/pin_manager.h`). This prevents pin-conflict bugs.
|
||||
3. **Identification in JSON info** — `UsermodManager::addToJsonInfo` emits each mod's ID into the `"um"` array; a unique ID makes the mod identifiable in that output.
|
||||
|
||||
If none of the above apply, the usermod may omit `getId()` (or return the default `USERMOD_ID_UNSPECIFIED`) and does **not** need an entry in `const.h`.
|
||||
|
||||
### Usermod `loop()`
|
||||
|
||||
- Called once per main loop iteration. Usermods should simply `return` when `!enabled`.
|
||||
- Frequency of calls varies with system load:
|
||||
* up to 2000 times/sec with few LEDs and little background activity,
|
||||
* between 20 and 300 times/second during high workload from effects and other usermods,
|
||||
* (worst case) down to 1-3 times/sec during FS activity or when serving lots of network API requests.
|
||||
|
||||
## CI/CD
|
||||
|
||||
CI runs on every push/PR via GitHub Actions (`.github/workflows/wled-ci.yml`):
|
||||
|
||||
1. `npm test` (web UI build validation)
|
||||
2. Firmware compilation for all default environments (~22 targets)
|
||||
3. Post-link validation of usermod linkage (`validate_modules.py`)
|
||||
|
||||
No automated linting is configured. Match existing code style in files you edit.
|
||||
|
||||
## General Rules
|
||||
|
||||
- Important: Repository language is **English**. This applies to source code (including comments), commit messages and any kind of documentation for developer or users.
|
||||
- The `docs/` folder is for developer/contributor information (coding conventions, architecture, etc.). User documentation is maintained in the [wled/WLED-Docs](https://github.com/wled/WLED-Docs) repository.
|
||||
- Never edit or commit auto-generated `wled00/html_*.h` / `wled00/js_*.h`.
|
||||
- When updating an existing PR, retain the original description. Only modify it to ensure technical accuracy. Add change logs after the existing description.
|
||||
- No force-push on open PRs!
|
||||
- Important: **Changes to `platformio.ini` require maintainer approval**!
|
||||
- PRs should respect `.gitignore` and not upload files like `platformio_override.ini`. PR authors may add buildenv examples for custom boards into `platformio_override.ini.sample`.
|
||||
- Remove dead/unused code — justify or delete it.
|
||||
- Verify feature-flag spelling exactly (misspellings are silently ignored by preprocessor).
|
||||
- Provide references when making analyses or recommendations. Support factual claims with verifiable citations, references or concrete evidence; **never fabricate citations**.
|
||||
- **Highlight user-visible breaking changes and ripple effects** during reviews. Ask for confirmation that these were introduced intentionally.
|
||||
|
||||
### Security Hardening
|
||||
|
||||
When writing or reviewing code in `wled00/`, `usermods/`, `wled00/data/`, or `.github/workflows/`,
|
||||
consult `docs/hardening.instructions.md` (concise checklist) and `docs/securecode.instructions.md` (detailed rules with examples).
|
||||
These files define WLED's threat model, trust boundary model, and WLED-specific constraints (no TLS baseline, no UDP authentication for protocol-defined
|
||||
multicast/broadcast, firewall-isolated deployment assumed).
|
||||
|
||||
### Attribution for AI-generated code
|
||||
|
||||
Using AI-generated code can hide the source of the inspiration / knowledge / sources it used.
|
||||
|
||||
- Document attribution of inspiration / knowledge / sources used in the code, e.g. link to GitHub repositories or other websites describing the principles / algorithms used.
|
||||
- When a larger block of code is generated by an AI tool, embed it into `// AI: below section was generated by an AI` ... `// AI: end` comments (see Comments section).
|
||||
- Every non-trivial AI-generated function should have a brief comment describing what it does. Explain parameters when their names alone are not self-explanatory.
|
||||
- AI-generated code must be well documented with meaningful comments that explain intent, assumptions, and non-obvious logic. Do not rephrase source code; explain concepts and reasoning.
|
||||
|
||||
### Supporting Reviews and Discussions
|
||||
|
||||
- **For "is it worth doing?" debates** about proposed reliability, safety, or data-integrity mechanisms (CRC checks, backups, power-loss protection): suggest a software **FMEA** (Failure Mode and Effects Analysis).
|
||||
Clarify the main feared events, enumerate failure modes, assess each mitigation's effectiveness per failure mode, note common-cause failures, and rate credibility for the typical WLED use case.
|
||||
+28
-175
@@ -1,188 +1,43 @@
|
||||
# Thank you for making WLED better!
|
||||
## Thank you for making WLED better!
|
||||
|
||||
WLED is a community-driven project, and every contribution matters! We appreciate your time and effort.
|
||||
Here are a few suggestions to make it easier for you to contribute!
|
||||
|
||||
Our maintainers are here for two things: **helping you** improve your code, and **keeping WLED** lean, efficient, and maintainable.
|
||||
We'll work with you to refine your contribution, but we'll also push back if something might create technical debt or add features without clear value. Don't take it personally - we're just protecting WLED's architecture while helping your contribution succeed!
|
||||
### Describe your PR
|
||||
|
||||
## Getting Started
|
||||
Please add a description of your proposed code changes. It does not need to be an exhaustive essay, however a PR with no description or just a few words might not get accepted, simply because very basic information is missing.
|
||||
|
||||
Here are a few suggestions to make it easier for you to contribute:
|
||||
|
||||
### Important Developer Infos
|
||||
* [Project Structure, Files and Directories](AGENTS.md#project-structure) (in our AI instructions)
|
||||
* [Instructions for creating usermods](AGENTS.md#usermod-pattern) (in our AI instructions)
|
||||
* KB: [Compiling WLED](https://kno.wled.ge/advanced/compiling-wled/) - slightly outdated but still helpful :-)
|
||||
* Arduino IDE is not supported any more. Use VSCode with the PlatformIO extension.
|
||||
* [Compiling in VSCode/Platformio](https://github.com/wled/WLED-Docs/issues/161) - modern way without command line or platformio.ini changes.
|
||||
* If you add a new feature, consider making a PR to [``wled-docs``](https://github.com/wled/WLED-Docs) for updating our official documentation.
|
||||
|
||||
### PR from a branch in your own fork
|
||||
Start your pull request (PR) in a branch of your own fork. Don't make a PR directly from your main branch.
|
||||
This lets you update your PR if needed, while you can work on other tasks in 'main' or in other branches.
|
||||
|
||||
> [!TIP]
|
||||
> **The easiest way to start your first PR**
|
||||
> When viewing a file in `wled/WLED`, click on the "pen" icon and start making changes.
|
||||
> When you choose to 'Commit changes', GitHub will automatically create a PR from your fork.
|
||||
>
|
||||
> <img width="295" height="134" alt="image: fork and edit" src="https://github.com/user-attachments/assets/f0dc7567-edcb-4409-a530-cd621ae9661f" />
|
||||
A good description helps us to review and understand your proposed changes. For example, you could say a few words about
|
||||
* what you try to achieve (new feature, fixing a bug, refactoring, security enhancements, etc.)
|
||||
* how your code works (short technical summary - focus on important aspects that might not be obvious when reading the code)
|
||||
* testing you performed, known limitations, open ends you possibly could not solve.
|
||||
* any areas where you like to get help from an experienced maintainer (yes WLED has become big 😉)
|
||||
|
||||
### Target branch for pull requests
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Please make all PRs against the `main` branch.
|
||||
|
||||
### Describing your PR
|
||||
|
||||
Please add a description of your proposed code changes.
|
||||
A PR with no description or just a few words might not get accepted, simply because very basic information is missing.
|
||||
No need to write an essay!
|
||||
|
||||
A good description helps us to review and understand your proposed changes. For example, you could say a few words about
|
||||
* What you try to achieve (new feature, fixing a bug, refactoring, security enhancements, etc.)
|
||||
* How your code works (short technical summary - focus on important aspects that might not be obvious when reading the code)
|
||||
* Testing you performed, known limitations, anything you couldn't quite solve.
|
||||
* Let us know if you'd like guidance from a maintainer (WLED is a big project 😉)
|
||||
|
||||
### Testing Your Changes
|
||||
|
||||
Before submitting:
|
||||
|
||||
- ✅ Does it compile?
|
||||
- ✅ Does your feature/fix actually work?
|
||||
- ✅ Did you break anything else?
|
||||
- ✅ Tested on actual hardware if possible?
|
||||
|
||||
Mention your testing in the PR description (e.g., "Tested on ESP32 + WS2812B").
|
||||
|
||||
## During Review
|
||||
|
||||
We're all volunteers, so reviews can take some time (longer during busy times).
|
||||
Don't worry - we haven't forgotten you! Feel free to ping after a week if there's no activity.
|
||||
Please make all PRs against the `main` branch.
|
||||
|
||||
### Updating your code
|
||||
While the PR is open, you can keep updating your branch - just push more commits! GitHub will automatically update your PR.
|
||||
While the PR is open - and under review by maintainers - you may be asked to modify your PR source code.
|
||||
You can simply update your own branch, and push changes in response to reviewer recommendations.
|
||||
Github will pick up the changes so your PR stays up-to-date.
|
||||
|
||||
You don't need to squash commits or clean up history - we'll handle that when merging.
|
||||
|
||||
> [!CAUTION]
|
||||
> [!CAUTION]
|
||||
> Do not use "force-push" while your PR is open!
|
||||
> It has many subtle and unexpected consequences on our GitHub repository.
|
||||
> For example, we regularly lose review comments when the PR author force-pushes code changes. Our review bot (coderabbit) may become unable to properly track changes, it gets confused or stops responding to questions.
|
||||
> So, pretty please, do not force-push.
|
||||
|
||||
> [!TIP]
|
||||
> Use [cherry-picking](https://docs.github.com/en/desktop/managing-commits/cherry-picking-a-commit-in-github-desktop) to copy commits from one branch to another.
|
||||
> It has many subtle and unexpected consequences on our github reposistory.
|
||||
> For example, we regularly lost review comments when the PR author force-pushes code changes. So, pretty please, do not force-push.
|
||||
|
||||
|
||||
### Responding to Reviews
|
||||
You can find a collection of very useful tips and tricks here: https://github.com/wled-dev/WLED/wiki/How-to-properly-submit-a-PR
|
||||
|
||||
When we ask for changes:
|
||||
|
||||
- **Add new commits** - please don't amend or force-push
|
||||
- **Reply in the PR** - let us know when you've addressed comments
|
||||
- **Ask questions** - if something's unclear, just ask!
|
||||
- **Be patient** - we're all volunteers here 😊
|
||||
|
||||
You can reference feedback in commit messages:
|
||||
> ```text
|
||||
> Fix naming per @Aircoookie's suggestion
|
||||
> ```
|
||||
|
||||
### Dealing with Merge Conflicts
|
||||
|
||||
Got conflicts with `main`? No worries - here's how to fix them:
|
||||
|
||||
**Using GitHub Desktop** (easier for beginners):
|
||||
|
||||
1. Click **Fetch origin**, then **Pull origin**
|
||||
2. If conflicts exist, GitHub Desktop will warn you - click **View conflicts**
|
||||
3. Open the conflicted files in your editor (VS Code, etc.)
|
||||
4. Remove the conflict markers (`<<<<<<<`, `=======`, `>>>>>>>`) and keep the correct code
|
||||
5. Save the files
|
||||
6. Back in GitHub Desktop, commit the merge (it'll suggest a message)
|
||||
7. Click **Push origin**
|
||||
|
||||
**Using command line**:
|
||||
|
||||
```bash
|
||||
git fetch origin
|
||||
git merge origin/main
|
||||
# Fix conflicts in your editor
|
||||
git add .
|
||||
git commit
|
||||
git push
|
||||
```
|
||||
|
||||
Either way works fine - pick what you're comfortable with! Merging is simpler than rebasing and keeps everything connected.
|
||||
|
||||
#### When you MUST rebase (really rare!)
|
||||
|
||||
Sometimes you might hit merge conflicts with `main` that are harder to solve. Here's what to try:
|
||||
|
||||
1. **Merge instead of rebase** (safest option):
|
||||
```bash
|
||||
git fetch origin
|
||||
git merge origin/main
|
||||
git push
|
||||
```
|
||||
Keeps review comments attached and CI results visible!
|
||||
|
||||
2. **Use cherry-picking** to copy commits between branches without rewriting history - [here's how](https://docs.github.com/en/desktop/managing-commits/cherry-picking-a-commit-in-github-desktop).
|
||||
|
||||
3. **If all else fails, use `--force-with-lease`** (not plain `--force`):
|
||||
```bash
|
||||
git rebase origin/main
|
||||
git push --force-with-lease
|
||||
```
|
||||
Then **leave a comment** explaining why you had to force-push, and be ready to re-address some feedback.
|
||||
|
||||
### Additional Resources
|
||||
Want to know more? Check out:
|
||||
- 📚 [GitHub Desktop documentation](https://docs.github.com/en/desktop) - if you prefer GUI tools
|
||||
|
||||
## After Approval
|
||||
Once approved, a maintainer will merge your PR (possibly squashing commits).
|
||||
Your contribution will be in the next WLED release - thank you! 🎉
|
||||
|
||||
|
||||
## Coding Guidelines
|
||||
|
||||
### Source Code from an AI agent or bot
|
||||
> [!IMPORTANT]
|
||||
> It's OK if you took help from an AI for writing your source code.
|
||||
>
|
||||
> AI tools can be very helpful, but as the contributor, **you're responsible for the code**.
|
||||
|
||||
* Make sure you really understand the AI-generated code, don't just accept it because it "seems to work".
|
||||
* Don't let the AI change existing code without double-checking by you as the contributor. Often, the result will not be complete. For example, previous source code comments may be lost.
|
||||
* Remember that AI is still "Often-Wrong" ;-)
|
||||
* If you don't feel confident using English, you can use AI for translating code comments and descriptions into English. AI bots are very good at understanding language. However, always check if the results are correct. The translation might still have wrong technical terms, or errors in some details.
|
||||
|
||||
#### Best Practice with AI
|
||||
|
||||
AI tools are powerful but "often wrong" - your judgment is essential! 😊
|
||||
|
||||
- ✅ **Understand the code** - As the person contributing to WLED, make sure you understand exactly what the AI-generated source code does
|
||||
- ✅ **Review carefully** - AI can lose comments, introduce bugs, or make unnecessary changes
|
||||
- ✅ **Be transparent** - Add comments `// AI: below section was generated by an AI` ... `// AI: end` around larger chunks
|
||||
- ✅ **Use AI for translation** - AI is great for translating comments to English (but verify technical terms!)
|
||||
|
||||
### Code style
|
||||
|
||||
Don't stress too much about style! When in doubt, just match the style in the files you're editing. 😊
|
||||
|
||||
Our review bot (coderabbit) has learned lots of detailed guides and hints - it will suggest them automatically when you submit a PR for review.
|
||||
If you are curious, these are the detailed guides:
|
||||
* [C++ Coding](docs/cpp.instructions.md)
|
||||
* [WebUi: HTML, JS, CSS](docs/web.instructions.md)
|
||||
|
||||
Below are the main rules used in the WLED repository:
|
||||
When in doubt, it is easiest to replicate the code style you find in the files you want to edit :)
|
||||
Below are the guidelines we use in the WLED repository.
|
||||
|
||||
#### Indentation
|
||||
|
||||
We use tabs for indentation in Web files (.html/.css/.js) and spaces (2 per indentation level) for all other files.
|
||||
We use tabs for Indentation in Web files (.html/.css/.js) and spaces (2 per indentation level) for all other files.
|
||||
You are all set if you have enabled `Editor: Detect Indentation` in VS Code.
|
||||
|
||||
#### Blocks
|
||||
@@ -200,7 +55,7 @@ if (a == b) {
|
||||
if (a == b) doStuff(a);
|
||||
```
|
||||
|
||||
Also acceptable (though the first style is usually easier to read):
|
||||
Acceptable - however the first variant is usually easier to read:
|
||||
```cpp
|
||||
if (a == b)
|
||||
{
|
||||
@@ -231,22 +86,20 @@ if( a==b ){
|
||||
#### Comments
|
||||
|
||||
Comments should have a space between the delimiting characters (e.g. `//`) and the comment text.
|
||||
We're gradually adopting this style - don't worry if you see older code without spaces!
|
||||
Note: This is a recent change, the majority of the codebase still has comments without spaces.
|
||||
|
||||
Good:
|
||||
```cpp
|
||||
// This is a short inline comment.
|
||||
```
|
||||
// This is a comment.
|
||||
|
||||
/* This is a CSS inline comment */
|
||||
|
||||
/*
|
||||
* This is a longer comment
|
||||
* This is a comment
|
||||
* wrapping over multiple lines,
|
||||
* used in WLED for file headers and function explanations
|
||||
*/
|
||||
```
|
||||
```css
|
||||
/* This is a CSS inline comment */
|
||||
```
|
||||
```html
|
||||
|
||||
<!-- This is an HTML comment -->
|
||||
```
|
||||
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
{
|
||||
"build": {
|
||||
"arduino":{
|
||||
"ldscript": "esp32s3_out.ld",
|
||||
"partitions": "default_8MB.csv"
|
||||
},
|
||||
"core": "esp32",
|
||||
"extra_flags": [
|
||||
"-DARDUINO_ADAFRUIT_MATRIXPORTAL_ESP32S3",
|
||||
"-DARDUINO_USB_CDC_ON_BOOT=1",
|
||||
"-DARDUINO_RUNNING_CORE=1",
|
||||
"-DARDUINO_EVENT_RUNNING_CORE=1",
|
||||
"-DBOARD_HAS_PSRAM"
|
||||
],
|
||||
"f_cpu": "240000000L",
|
||||
"f_flash": "80000000L",
|
||||
"flash_mode": "qio",
|
||||
"hwids": [
|
||||
[
|
||||
"0x239A",
|
||||
"0x8125"
|
||||
],
|
||||
[
|
||||
"0x239A",
|
||||
"0x0125"
|
||||
],
|
||||
[
|
||||
"0x239A",
|
||||
"0x8126"
|
||||
]
|
||||
],
|
||||
"mcu": "esp32s3",
|
||||
"variant": "adafruit_matrixportal_esp32s3"
|
||||
},
|
||||
"connectivity": [
|
||||
"bluetooth",
|
||||
"wifi"
|
||||
],
|
||||
"debug": {
|
||||
"openocd_target": "esp32s3.cfg"
|
||||
},
|
||||
"frameworks": [
|
||||
"arduino",
|
||||
"espidf"
|
||||
],
|
||||
"name": "Adafruit MatrixPortal ESP32-S3 for WLED",
|
||||
"upload": {
|
||||
"flash_size": "8MB",
|
||||
"maximum_ram_size": 327680,
|
||||
"maximum_size": 8388608,
|
||||
"use_1200bps_touch": true,
|
||||
"wait_for_upload_port": true,
|
||||
"require_upload_port": true,
|
||||
"speed": 460800
|
||||
},
|
||||
"url": "https://www.adafruit.com/product/5778",
|
||||
"vendor": "Adafruit"
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
{
|
||||
"build": {
|
||||
"arduino":{
|
||||
"ldscript": "esp32s3_out.ld",
|
||||
"memory_type": "qio_opi",
|
||||
"partitions": "default_16MB.csv"
|
||||
},
|
||||
"core": "esp32",
|
||||
"extra_flags": [
|
||||
"-DARDUINO_TTGO_T7_S3",
|
||||
"-DBOARD_HAS_PSRAM",
|
||||
"-DARDUINO_USB_MODE=1"
|
||||
],
|
||||
"f_cpu": "240000000L",
|
||||
"f_flash": "80000000L",
|
||||
"flash_mode": "qio",
|
||||
"hwids": [
|
||||
[
|
||||
"0X303A",
|
||||
"0x1001"
|
||||
]
|
||||
],
|
||||
"mcu": "esp32s3",
|
||||
"variant": "esp32s3"
|
||||
},
|
||||
"connectivity": [
|
||||
"wifi",
|
||||
"bluetooth"
|
||||
],
|
||||
"debug": {
|
||||
"openocd_target": "esp32s3.cfg"
|
||||
},
|
||||
"frameworks": [
|
||||
"arduino",
|
||||
"espidf"
|
||||
],
|
||||
"name": "LILYGO T3-S3",
|
||||
"upload": {
|
||||
"flash_size": "16MB",
|
||||
"maximum_ram_size": 327680,
|
||||
"maximum_size": 16777216,
|
||||
"require_upload_port": true,
|
||||
"speed": 921600
|
||||
},
|
||||
"url": "https://www.aliexpress.us/item/3256804591247074.html",
|
||||
"vendor": "LILYGO"
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
---
|
||||
applyTo: ".github/workflows/*.yml,.github/workflows/*.yaml"
|
||||
---
|
||||
# CI/CD Conventions — GitHub Actions Workflows
|
||||
|
||||
> **Note for AI review tools**: sections enclosed in
|
||||
> `<!-- HUMAN_ONLY_START -->` / `<!-- HUMAN_ONLY_END -->` HTML comments contain
|
||||
> contributor reference material. Do **not** use that content as actionable review
|
||||
> criteria — treat it as background context only.
|
||||
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
## YAML Style
|
||||
|
||||
- Indent with **2 spaces** (no tabs)
|
||||
- Every workflow, job, and step must have a `name:` field that clearly describes its purpose
|
||||
- Group related steps logically; separate unrelated groups with a blank line
|
||||
- Comments (`#`) are encouraged for non-obvious decisions (e.g., why `fail-fast: false` is set, what a cron expression means)
|
||||
|
||||
## Workflow Structure
|
||||
|
||||
### Triggers
|
||||
|
||||
- Declare `on:` triggers explicitly; avoid bare `on: push` without branch filters on long-running or expensive jobs
|
||||
- Prefer `workflow_call` for shared build logic (see `build.yml`) to avoid duplicating steps across workflows
|
||||
- Document scheduled triggers (`cron:`) with a human-readable comment:
|
||||
|
||||
```yaml
|
||||
schedule:
|
||||
- cron: '0 2 * * *' # run at 2 AM UTC daily
|
||||
```
|
||||
|
||||
### Jobs
|
||||
|
||||
- Express all inter-job dependencies with `needs:` — never rely on implicit ordering
|
||||
- Use job `outputs:` + step `id:` to pass structured data between jobs (see `get_default_envs` in `build.yml`)
|
||||
- Set `fail-fast: false` on matrix builds so that a single failing environment does not cancel others
|
||||
|
||||
### Runners
|
||||
|
||||
- Pin to a specific Ubuntu version (`ubuntu-22.04`, `ubuntu-24.04`) rather than `ubuntu-latest` for reproducible builds
|
||||
- Only use `ubuntu-latest` in jobs where exact environment reproducibility is not required (e.g., trivial download/publish steps)
|
||||
|
||||
### Tool and Language Versions
|
||||
|
||||
- Pin tool versions explicitly:
|
||||
```yaml
|
||||
python-version: '3.12'
|
||||
```
|
||||
- Do not rely on the runner's pre-installed tool versions — always install via a versioned setup action
|
||||
|
||||
### Caching
|
||||
|
||||
- Always cache package managers and build tool directories when the job installs dependencies:
|
||||
```yaml
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
```
|
||||
- Include the environment name or a relevant identifier in cache keys when building multiple targets
|
||||
|
||||
### Artifacts
|
||||
|
||||
- Name artifacts with enough context to be unambiguous (e.g., `firmware-${{ matrix.environment }}`)
|
||||
- Avoid uploading artifacts that will never be consumed downstream
|
||||
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
---
|
||||
|
||||
## Security
|
||||
|
||||
Important: Several current workflows still violate parts of the baseline below - migration is in progress.
|
||||
|
||||
### Permissions — Least Privilege
|
||||
|
||||
Declare explicit `permissions:` blocks. The default token permissions are broad; scope them to the minimum required:
|
||||
|
||||
```yaml
|
||||
permissions:
|
||||
contents: read # for checkout
|
||||
```
|
||||
|
||||
For jobs that publish releases or write to the repository:
|
||||
|
||||
```yaml
|
||||
permissions:
|
||||
contents: write # create/update releases
|
||||
```
|
||||
|
||||
A common safe baseline for build-only jobs:
|
||||
|
||||
```yaml
|
||||
permissions:
|
||||
contents: read
|
||||
```
|
||||
|
||||
### Supply Chain — Action Pinning
|
||||
|
||||
**Third-party actions** (anything outside the `actions/` and `github/` namespaces) should be pinned to a specific release tag. Branch pins (`@main`, `@master`) are **not allowed** — they can be updated by the action author at any time without notice:
|
||||
|
||||
```yaml
|
||||
# ✅ Acceptable — specific version tag. SHA pinning recommended for more security, as @v2 is still a mutable tag.
|
||||
uses: softprops/action-gh-release@v2
|
||||
|
||||
# ❌ Not acceptable — mutable branch reference
|
||||
uses: andelf/nightly-release@main
|
||||
```
|
||||
|
||||
SHA pinning (e.g., `uses: someorg/some-action@abc1234`) is the most secure option for third-party actions; it is recommended when auditing supply-chain risk is a priority. At minimum, always use a specific version tag.
|
||||
|
||||
**First-party actions** (`actions/checkout`, `actions/cache`, `actions/upload-artifact`, etc.) pinned to a major version tag (e.g., `@v4`) are acceptable because GitHub maintains and audits these.
|
||||
|
||||
When adding a new third-party action:
|
||||
1. Check that the action's repository is actively maintained
|
||||
2. Review the action's source before adding it
|
||||
3. Prefer well-known, widely-used actions over obscure ones
|
||||
|
||||
### Credentials and Secrets
|
||||
|
||||
- Use `${{ secrets.GITHUB_TOKEN }}` for operations within the same repository — it is automatically scoped and rotated
|
||||
- Never commit secrets, tokens, or passwords into workflow files or any tracked file
|
||||
- Never print secrets in `run:` steps, even with `echo` — GitHub masks known secrets but derived values are not automatically masked
|
||||
- Scope secrets to the narrowest step that needs them using `env:` at the step level, not at the workflow level:
|
||||
|
||||
```yaml
|
||||
# ✅ Scoped to the step that needs it
|
||||
- name: Create release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# ❌ Unnecessarily broad
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
```
|
||||
|
||||
- Personal Access Tokens (PATs, stored as repository secrets) should have the minimum required scopes and should be rotated periodically
|
||||
|
||||
### Script Injection
|
||||
|
||||
`${{ }}` expressions are evaluated before the shell script runs. If an expression comes from untrusted input (PR titles, issue bodies, branch names from forks), it can inject arbitrary shell commands.
|
||||
|
||||
**Never** interpolate `github.event.*` values directly into a `run:` step:
|
||||
|
||||
```yaml
|
||||
# ❌ Injection risk — PR title is attacker-controlled
|
||||
- run: echo "${{ github.event.pull_request.title }}"
|
||||
|
||||
# ✅ Safe — value passed through an environment variable
|
||||
- env:
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
run: echo "$PR_TITLE"
|
||||
```
|
||||
|
||||
This rule applies to any value that originates outside the repository (issue bodies, labels, comments, commit messages from forks).
|
||||
|
||||
### Pull Request Workflows
|
||||
|
||||
- Workflows triggered by `pull_request` from a fork run with **read-only** token permissions and no access to repository secrets — this is intentional and correct
|
||||
- Do not use `pull_request_target` unless you fully understand the security implications; it runs in the context of the base branch and *does* have secret access, making it a common attack surface
|
||||
@@ -1,528 +0,0 @@
|
||||
---
|
||||
applyTo: "**/*.cpp,**/*.h,**/*.hpp,**/*.ino"
|
||||
---
|
||||
# C++ Coding Conventions
|
||||
|
||||
> **Note for AI review tools**: sections enclosed in
|
||||
> `<!-- HUMAN_ONLY_START -->` / `<!-- HUMAN_ONLY_END -->` HTML comments contain
|
||||
> contributor reference material. Do **not** use that content as actionable review
|
||||
> criteria — treat it as background context only.
|
||||
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
<!-- hiding this reference, to avoid cyclic "include" loops -->
|
||||
See also: [CONTRIBUTING.md](../CONTRIBUTING.md) for general style guidelines that apply to all contributors.
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
|
||||
## Formatting
|
||||
|
||||
- Indent with **2 spaces** (no tabs in C++ files)
|
||||
- Opening braces on the same line is preferred (K&R style). Brace on a separate line (Allman style) is acceptable
|
||||
- Single-statement `if` bodies may omit braces: `if (a == b) doStuff(a);`
|
||||
- Space between keyword and parenthesis: `if (...)`, `for (...)`. No space between function name and parenthesis: `doStuff(a)`
|
||||
- No enforced line-length limit; wrap when a line exceeds your editor width
|
||||
|
||||
## Naming
|
||||
|
||||
- **camelCase** for functions and variables: `setValuesFromMainSeg()`, `effectCurrent`
|
||||
- **PascalCase** for classes and structs: `PinManagerClass`, `BusConfig`
|
||||
- **PascalCase** for enum values: `PinOwner::BusDigital`
|
||||
- **UPPER_CASE** for macros and constants: `WLED_MAX_USERMODS`, `DEFAULT_CLIENT_SSID`
|
||||
|
||||
## General
|
||||
|
||||
- Follow the existing style in the file you are editing
|
||||
- If possible, use `static` for local (C-style) variables and functions (keeps the global namespace clean)
|
||||
- Avoid unexplained "magic numbers". Prefer named constants (`constexpr`) or C-style `#define` constants for repeated numbers that have the same meaning
|
||||
- Include `"wled.h"` as the primary project header where needed
|
||||
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
## Header Guards
|
||||
|
||||
Most headers use `#ifndef` / `#define` guards. Some newer headers add `#pragma once` before the guard:
|
||||
|
||||
```cpp
|
||||
#ifndef WLED_EXAMPLE_H
|
||||
#define WLED_EXAMPLE_H
|
||||
// ...
|
||||
#endif // WLED_EXAMPLE_H
|
||||
```
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
|
||||
## Comments
|
||||
|
||||
- `//` for inline comments, `/* ... */` for block comments. Always put a space after `//`
|
||||
- **AI attribution:** When a larger block of code is generated by an AI tool, mark it with an `// AI:` comment so reviewers know to scrutinize it:
|
||||
|
||||
```cpp
|
||||
// AI: below section was generated by an AI
|
||||
void calculateCRC(const uint8_t* data, size_t len) {
|
||||
...
|
||||
}
|
||||
// AI: end
|
||||
```
|
||||
|
||||
Single-line AI-assisted edits do not need the marker — use it when the AI produced a contiguous block that a human did not write line-by-line.
|
||||
|
||||
<!-- 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:
|
||||
|
||||
```cpp
|
||||
/* *****
|
||||
* Apply gamma correction to a single color channel.
|
||||
* @param value raw 8-bit channel value (0–255)
|
||||
* @param gamma gamma exponent (typically 2.8)
|
||||
* @return corrected 8-bit value
|
||||
***** */
|
||||
uint8_t gammaCorrect(uint8_t value, float gamma);
|
||||
```
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
|
||||
Short accessor-style functions (getters/setters, one-liners) may skip this if their purpose is obvious from the name.
|
||||
|
||||
## Preprocessor & Feature Flags
|
||||
|
||||
- Prefer compile-time feature flags (`#ifdef` / `#ifndef`) over runtime checks where possible
|
||||
- Platform differentiation: `ARDUINO_ARCH_ESP32` vs `ESP8266`
|
||||
- PSRAM availability: `BOARD_HAS_PSRAM`
|
||||
|
||||
## Error Handling
|
||||
|
||||
- `DEBUG_PRINTF()` / `DEBUG_PRINTLN()` for developer diagnostics (compiled out unless `-D WLED_DEBUG`)
|
||||
- Don't rely on C++ exceptions — use return codes (`-1` / `false` for errors) and global flags (e.g. `errorFlag = ERR_LOW_MEM`). Some builds don't support C++ exceptions.
|
||||
|
||||
## Strings
|
||||
|
||||
- Use `const char*` for temporary/parsed strings
|
||||
- Avoid `String` (Arduino heap-allocated string) in hot paths; acceptable in config/setup code
|
||||
- Use `F("string")` for string constants (major RAM win on ESP8266; mostly overload/type compatibility on ESP32)
|
||||
- Store repeated strings as `static const char[] PROGMEM`
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
|
||||
On **ESP8266** this explicitly stores the string in flash (PROGMEM), saving precious RAM — every byte counts on that platform.
|
||||
On **ESP32**, `PROGMEM` is a no-op and string literals already reside in flash/rodata, so `F()` yields little RAM benefit but remains harmless (it satisfies `__FlashStringHelper*` overloads that some APIs expect).
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
|
||||
```cpp
|
||||
DEBUG_PRINTLN(F("WS client connected.")); // string stays in flash, not RAM
|
||||
DEBUG_PRINTF_P(PSTR("initE: Ignoring attempt for invalid ethernetType (%d)\n"), ethernetType); // format string stays in flash
|
||||
```
|
||||
|
||||
## Memory
|
||||
|
||||
- **PSRAM-aware allocation**: use `d_malloc()` (prefer DRAM), `p_malloc()` (prefer PSRAM) from `fcn_declare.h`
|
||||
- **Avoid Variable Length Arrays (VLAs)**: FreeRTOS task stacks are typically 2–8 KB. A runtime-sized VLA can silently exhaust the stack. Use fixed-size arrays or heap allocation (`d_malloc` / `p_malloc`). Any VLA must be explicitly justified in source or PR.
|
||||
<!-- 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.
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
- **Larger buffers** (LED data, JSON documents) should use PSRAM when available and technically feasible
|
||||
- **Hot-path**: some data should stay in DRAM or IRAM for performance reasons
|
||||
- Memory efficiency matters, but is less critical on boards with PSRAM
|
||||
|
||||
Heap fragmentation is a concern:
|
||||
<!-- 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`
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
`const` is a promise to the compiler that a value (or object) will not change - a function declared with a `const char* message` parameter is not allowed to modify the content of `message`.
|
||||
This pattern enables optimizations and makes intent clear to reviewers.
|
||||
|
||||
`constexpr` allows to define constants that are *guaranteed* to be evaluated by the compiler (zero run-time costs).
|
||||
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
- For function parameters that are read-only, prefer `const &` or `const`.
|
||||
|
||||
### `const` locals
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
* Adding `const` to a local variable that is only assigned once is optional, but *not* strictly necessary.
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
* In hot-path code, `const` on cached locals may help the compiler keep values in registers.
|
||||
```cpp
|
||||
const uint_fast16_t cols = vWidth();
|
||||
const uint_fast16_t rows = vHeight();
|
||||
```
|
||||
|
||||
### `const` references to avoid copies
|
||||
- Pass objects by `const &` (or `&`) instead of copying them implicitly.
|
||||
- Use `const &` (or `&`) inside loops - This avoids constructing temporary objects on every access.
|
||||
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
```cpp
|
||||
const auto &m = _mappings[i]; // reference, not a copy (bus_manager.cpp)
|
||||
Segment& sourcesegment = strip.getSegment(sourceid); // alias — avoids creating a temporary Segment instance
|
||||
```
|
||||
|
||||
For function parameters that are read-only, prefer `const &`:
|
||||
```cpp
|
||||
BusManager::add(const BusConfig &bc, bool placeholder) {
|
||||
```
|
||||
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
- Class **Data Members:** Avoid reference data members (`T&` or `const T&`) in a class.
|
||||
A reference member can outlive the object it refers to, causing **dangling reference** bugs that are hard to diagnose. Prefer value storage or use a pointer and document the expected lifetime.
|
||||
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
<!-- hidden from AI for now - codebase is not compliant to this rule (slowly migrating) -->
|
||||
### `constexpr` over `#define`
|
||||
|
||||
- Prefer `constexpr` for compile-time constants. Unlike `#define`, `constexpr` respects scope and type safety, keeping the global namespace clean.
|
||||
|
||||
```cpp
|
||||
// Prefer:
|
||||
constexpr uint32_t TWO_CHANNEL_MASK = 0x00FF00FF;
|
||||
constexpr int WLED_MAX_BUSSES = WLED_MAX_DIGITAL_CHANNELS + WLED_MAX_ANALOG_CHANNELS;
|
||||
|
||||
// Avoid (when possible):
|
||||
#define TWO_CHANNEL_MASK 0x00FF00FF
|
||||
```
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
|
||||
### `static_assert` over `#error`
|
||||
|
||||
- Use `static_assert` instead of the C-style `#if … #error … #endif` pattern when validating compile-time constants. It provides a clear message and works with `constexpr` values.
|
||||
- `#define` and `#if ... #else ... #endif` is still needed for conditional-compilation guards and build-flag-overridable values.
|
||||
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
```cpp
|
||||
// Prefer:
|
||||
constexpr int WLED_MAX_BUSSES = WLED_MAX_DIGITAL_CHANNELS + WLED_MAX_ANALOG_CHANNELS;
|
||||
static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit");
|
||||
|
||||
// Avoid:
|
||||
#if (WLED_MAX_BUSSES > 32)
|
||||
#error "WLED_MAX_BUSSES exceeds hard limit"
|
||||
#endif
|
||||
```
|
||||
|
||||
```cpp
|
||||
// using static_assert() to validate enumerated types (zero cost at runtime)
|
||||
static_assert(0u == static_cast<uint8_t>(PinOwner::None),
|
||||
"PinOwner::None must be zero, so default array initialization works as expected");
|
||||
```
|
||||
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
### `static` and `const` class methods
|
||||
|
||||
#### `const` member functions
|
||||
|
||||
Marking a member function `const` tells the compiler that it does not modify the object's state:
|
||||
|
||||
```cpp
|
||||
uint16_t length() const { return _len; }
|
||||
bool isActive() const { return _active; }
|
||||
```
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
Benefits for GCC/Xtensa/RISC-V:
|
||||
- The compiler knows the method cannot write to `this`, so it is free to **keep member values in registers** across the call and avoid reload barriers.
|
||||
- `const` methods can be called on `const` objects and `const` references — essential when passing large objects as `const &` to avoid copying.
|
||||
- `const` allows the compiler to **eliminate redundant loads**: if a caller already has a member value cached, the compiler can prove the `const` call cannot invalidate it.
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
|
||||
Declare getter, query, or inspection methods `const`. If you need to mark a member `mutable` to work around this (e.g. for a cache or counter), document the reason.
|
||||
|
||||
#### `static` member functions
|
||||
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
A `static` member function has no implicit `this` pointer. This has two distinct advantages:
|
||||
|
||||
1. **Smaller code, faster calls**: no `this` is passed in a register. On Xtensa and RISC-V, this removes one register argument from every call site and prevents the compiler from emitting `this`-preservation code around inlined blocks.
|
||||
2. **Better inlining**: GCC can inline a `static` method with more certainty because it cannot be overridden by a derived class (no virtual dispatch ambiguity) and has no aliasing concern through `this`.
|
||||
|
||||
Use `static` for any method that does not need access to instance members:
|
||||
|
||||
```cpp
|
||||
// Factory / utility — no instance needed:
|
||||
static BusConfig fromJson(JsonObject obj);
|
||||
|
||||
// Pure computation helpers:
|
||||
static uint8_t gamma8(uint8_t val);
|
||||
static uint32_t colorBalance(uint32_t color, uint8_t r, uint8_t g, uint8_t b);
|
||||
```
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
|
||||
`static` communicates intent clearly: a reviewer immediately knows the method is stateless and safe to call without a fully constructed object.
|
||||
|
||||
> **Rule of thumb**: if a method does not read or write any member variable, make it `static`. If it only reads member variables, make it `const`. Note: `static` methods cannot also be `const`-qualified because there is no implicit `this` pointer to be const — just use `static`. Both qualifiers reduce coupling and improve generated code on all ESP32 targets.
|
||||
|
||||
---
|
||||
|
||||
## Hot-Path Optimization
|
||||
|
||||
The hot path is the per-frame pixel pipeline: **Segment → Strip → BusManager → Bus(Digital,HUB75,Network) or PolyBus → LED driver, plus ``WS2812FX::show()`` and below**.
|
||||
Speed is the priority here. The patterns below are taken from existing hot-path code (`FX_fcn.cpp`, `FX_2Dfcn.cpp`, `bus_manager.cpp`, `colors.cpp`) and should be followed when modifying these files.
|
||||
|
||||
Note: `FX.cpp` (effect functions) is written by many contributors and has diverse styles — that is acceptable.
|
||||
|
||||
### Function Attributes
|
||||
|
||||
Stack the appropriate attributes on hot-path functions. Defined in `const.h`:
|
||||
|
||||
| Attribute | Meaning | When to use |
|
||||
|---|---|---|
|
||||
| `__attribute__((hot))` | Branch-prediction hint | hot-path functions with complex logic |
|
||||
| `IRAM_ATTR` | Place in fast IRAM (ESP32) | Critical per-pixel functions (e.g. `BusDigital::setPixelColor`) |
|
||||
| `IRAM_ATTR_YN` | IRAM on ESP32, no-op on ESP8266 | Hot functions that ESP8266 can't fit in IRAM |
|
||||
| `WLED_O2_ATTR` | Force `-O2` optimization | Most hot-path functions |
|
||||
| `WLED_O3_ATTR` | Force `-O3,fast-math` | Innermost color math (e.g. `color_blend`) |
|
||||
| `[[gnu::hot]] inline` | Modern C++ attribute + inline | Header-defined accessors (e.g. `progress()`, `currentBri()`) |
|
||||
|
||||
Note: `WLED_O3_ATTR` sometimes causes performance loss compared to `WLED_O2_ATTR`. Choose optimization levels based on test results.
|
||||
|
||||
Example signature:
|
||||
|
||||
```cpp
|
||||
void IRAM_ATTR_YN WLED_O2_ATTR __attribute__((hot)) Segment::setPixelColor(unsigned i, uint32_t c)
|
||||
```
|
||||
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
### Cache Members to Locals Before Loops
|
||||
|
||||
Copy class members and virtual-call results to local variables before entering a loop:
|
||||
|
||||
```cpp
|
||||
uint_fast8_t count = numBusses; // avoid repeated member access
|
||||
for (uint_fast8_t i = 0; i < count; i++) {
|
||||
Bus* const b = busses[i]; // const pointer hints to compiler
|
||||
uint_fast16_t bstart = b->getStart();
|
||||
uint_fast16_t blen = b->getLength();
|
||||
...
|
||||
}
|
||||
```
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
|
||||
### Unsigned Range Check
|
||||
|
||||
Replace two-comparison range tests with a single unsigned subtraction:
|
||||
|
||||
```cpp
|
||||
// Instead of: if (pix >= bstart && pix < bstart + blen)
|
||||
if ((uint_fast16_t)(pix - bstart) < blen) // also catches negative pix via unsigned underflow
|
||||
```
|
||||
|
||||
### Early Returns
|
||||
|
||||
Guard every hot-path function with the cheapest necessary checks first:
|
||||
|
||||
```cpp
|
||||
if (!isActive()) return; // inactive segment
|
||||
if (unsigned(i) >= vLength()) return; // bounds check (catches negative i too)
|
||||
```
|
||||
|
||||
### Avoid Nested Calls — Fast Path / Complex Path
|
||||
|
||||
Avoid calling non-inline functions or making complex decisions inside per-pixel hot-path code. When a function has both a common simple case and a rare complex case, split it into two variants and choose once per frame rather than per pixel.
|
||||
|
||||
General rules:
|
||||
- Keep fast-path functions free of non-inline calls, multi-way branches, and complex switch-case decisions.
|
||||
- Hoist per-frame decisions (e.g. simple vs. complex segment) out of the per-pixel loop.
|
||||
- Code duplication between fast/slow variants is acceptable to keep the fast path lean.
|
||||
|
||||
### Function Pointers to Eliminate Repeated Decisions
|
||||
|
||||
When the same decision (e.g. "which drawing routine?") would be evaluated for every pixel, assign the chosen variant to a function pointer once and let the inner loop call through the pointer. This removes the branch entirely — the calling code (e.g. the GIF decoder loop) only ever invokes one function per frame, with no per-pixel decision.
|
||||
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
`image_loader.cpp` demonstrates the pattern: `calculateScaling()` picks the best drawing callback once per frame based on segment dimensions and GIF size, then passes it to the decoder via `setDrawPixelCallback()`:
|
||||
|
||||
```cpp
|
||||
// calculateScaling() — called once per frame
|
||||
if ((perPixelX < 2) && (perPixelY < 2))
|
||||
decoder.setDrawPixelCallback(drawPixelCallbackDownScale2D); // downscale-only variant
|
||||
else
|
||||
decoder.setDrawPixelCallback(drawPixelCallback2D); // full-scaling variant
|
||||
```
|
||||
|
||||
Each callback is a small, single-purpose function with no internal branching — the decoder's per-pixel loop never re-evaluates which strategy to use.
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
### Template Specialization (Advanced)
|
||||
|
||||
Templates can eliminate runtime decisions by generating separate code paths at compile time. For example, a pixel setter could be templated on color order or channel count so the compiler removes dead branches and produces tight, specialized machine code:
|
||||
|
||||
```cpp
|
||||
template<bool hasWhite>
|
||||
void setChannel(uint8_t* out, uint32_t col) {
|
||||
out[0] = R(col); out[1] = G(col); out[2] = B(col);
|
||||
if constexpr (hasWhite) out[3] = W(col); // compiled out when hasWhite==false
|
||||
}
|
||||
```
|
||||
|
||||
Use sparingly — each instantiation duplicates code in flash. On ESP8266 and small-flash ESP32 boards this can exhaust IRAM/flash. Prefer templates only when the hot path is measurably faster and the number of instantiations is small (2–4).
|
||||
|
||||
### RAII Lock-Free Synchronization (Advanced)
|
||||
|
||||
Where contention is rare and the critical section is short, consider replacing mutex-based locking with lock-free techniques using `std::atomic` and RAII scoped guards. A scoped guard sets a flag on construction and clears it on destruction, guaranteeing cleanup even on early return:
|
||||
|
||||
```cpp
|
||||
struct ScopedBusyFlag {
|
||||
std::atomic<bool>& flag;
|
||||
bool acquired;
|
||||
ScopedBusyFlag(std::atomic<bool>& f) : flag(f), acquired(false) {
|
||||
bool expected = false;
|
||||
acquired = flag.compare_exchange_strong(expected, true);
|
||||
}
|
||||
~ScopedBusyFlag() { if (acquired) flag.store(false); }
|
||||
explicit operator bool() const { return acquired; }
|
||||
};
|
||||
|
||||
// Usage
|
||||
static std::atomic<bool> busySending{false};
|
||||
ScopedBusyFlag guard(busySending);
|
||||
if (!guard) return; // another task is already sending
|
||||
// ... do work — flag auto-clears when guard goes out of scope
|
||||
```
|
||||
|
||||
This avoids FreeRTOS semaphore overhead and the risk of forgetting to return a semaphore. There are no current examples of this pattern in the codebase — consult with maintainers before introducing it in new code, to ensure it aligns with the project's synchronization conventions.
|
||||
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
### Pre-Compute Outside Loops
|
||||
|
||||
Move invariant calculations before the loop. Pre-compute reciprocals to replace division with multiplication.
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
```cpp
|
||||
const uint_fast16_t cols = virtualWidth();
|
||||
const uint_fast16_t rows = virtualHeight();
|
||||
uint_fast8_t fadeRate = (255U - rate) >> 1;
|
||||
float mappedRate_r = 1.0f / (float(fadeRate) + 1.1f); // reciprocal — avoid division inside loop
|
||||
```
|
||||
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
### Parallel Channel Processing
|
||||
|
||||
Process R+B and W+G channels simultaneously using the two-channel mask pattern:
|
||||
|
||||
```cpp
|
||||
constexpr uint32_t TWO_CHANNEL_MASK = 0x00FF00FF;
|
||||
uint32_t rb = (((c1 & TWO_CHANNEL_MASK) * amount) >> 8) & TWO_CHANNEL_MASK;
|
||||
uint32_t wg = (((c1 >> 8) & TWO_CHANNEL_MASK) * amount) & ~TWO_CHANNEL_MASK;
|
||||
return rb | wg;
|
||||
```
|
||||
|
||||
### Bit Shifts Over Division (mainly for RISC-V boards)
|
||||
|
||||
ESP32 and ESP32-S3 (Xtensa core) have a fast "integer divide" instruction, so manual shifts rarely help.
|
||||
On RISC-V targets (ESP32-C3/C6/P4) and ESP8266, prefer explicit bit-shifts for power-of-two arithmetic — the compiler does **not** always convert divisions to shifts.
|
||||
Always use unsigned operands for right shifts; signed right-shift is implementation-defined.
|
||||
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
On RISC-V-based boards (ESP32-C3, ESP32-C6, ESP32-C5) explicit shifts can be beneficial.
|
||||
```cpp
|
||||
position >> 3 // instead of position / 8
|
||||
(255U - rate) >> 1 // instead of (255 - rate) / 2
|
||||
i & 0x0007 // instead of i % 8
|
||||
```
|
||||
|
||||
**Important**: The bit-shifted expression should be unsigned. On some MCUs, "signed right-shift" is implemented by an "arithmetic shift right" that duplicates the sign bit: ``0b1010 >> 1 = 0b1101``.
|
||||
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
### Static Caching for Expensive Computations
|
||||
|
||||
Cache results in static locals when the input rarely changes between calls:
|
||||
|
||||
```cpp
|
||||
static uint16_t lastKelvin = 0;
|
||||
static byte correctionRGB[4] = {255,255,255,0};
|
||||
if (lastKelvin != kelvin) {
|
||||
colorKtoRGB(kelvin, correctionRGB); // expensive — only recalculate when input changes
|
||||
lastKelvin = kelvin;
|
||||
}
|
||||
```
|
||||
|
||||
### Inlining Strategy
|
||||
|
||||
- Move frequently-called small functions to headers for inlining (e.g. `Segment::setPixelColorRaw` is in `FX.h`)
|
||||
- Use `static inline` for file-local helpers
|
||||
|
||||
### Math & Trigonometric Functions
|
||||
|
||||
- WLED uses a custom `fastled_slim` library. The old FastLED trig aliases (`sin8`, `cos8`, `sin16`, `cos16`) **no longer exist and cause a compile error** — use `sin8_t()`, `cos8_t()`, `sin16_t()`, `cos16_t()` instead. For float approximations use `sin_approx()` / `cos_approx()` instead of `sinf()` / `cosf()`. Replace FastLED noise aliases (`inoise8`, `inoise16`) with `perlin8`, `perlin16`.
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
|
||||
| ❌ Do not use (compile error) | ✅ Use instead | Source |
|
||||
|---|---|---|
|
||||
| `sin8()`, `cos8()` | `sin8_t()`, `cos8_t()` | `fastled_slim.h` → `wled_math.cpp` |
|
||||
| `sin16()`, `cos16()` | `sin16_t()`, `cos16_t()` | `fastled_slim.h` → `wled_math.cpp` |
|
||||
| `sinf()`, `cosf()` | `sin_approx()`, `cos_approx()` | `wled_math.cpp` |
|
||||
| `atan2f()`, `atan2()` | `atan2_t()` | `wled_math.cpp` |
|
||||
| `sqrt()` on integers | `sqrt32_bw()` | `fcn_declare.h` → `wled_math.cpp` |
|
||||
| `sqrtf()` on floats | `sqrtf()` (acceptable) | — no WLED replacement |
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
|
||||
---
|
||||
|
||||
## `delay()` vs `yield()` in ESP32 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()` stalls the entire system unless `yield()` was called inside.
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
|
||||
- On ESP32, `delay()` is generally allowed, as it helps to efficiently manage CPU usage of all tasks.
|
||||
- On ESP8266, only use `delay()` and `yield()` in the main `loop()` context. If not sure, protect with `if (can_yield()) ...`.
|
||||
- Do *not* use `delay()` in effects (FX.cpp) or in the hot pixel path.
|
||||
- `delay()` on the bus-level is allowed, it might be needed to achieve exact timing in LED drivers.
|
||||
|
||||
### IDLE Watchdog and Custom Tasks on ESP32
|
||||
|
||||
- In arduino-esp32, `yield()` calls `vTaskDelay(0)`, which only switches to tasks at equal or higher priority — the IDLE task (priority 0) is never reached.
|
||||
- **Do not use `yield()` to pace ESP32 tasks or assume it feeds any watchdog**.
|
||||
- **Custom `xTaskCreate()` tasks must call `delay(1)` in their loop, not `yield()`.** Without a real blocking call, the IDLE task is starved. The IDLE watchdog panic is the first visible symptom — but the damage starts earlier: deleted task memory leaks, software timers stop firing, light sleep is disabled, and Wi-Fi/BT idle hooks don't run. Structure custom tasks like this:
|
||||
```cpp
|
||||
// WRONG — IDLE task is never scheduled; yield() does not feed the idle task watchdog.
|
||||
void myTask(void*) {
|
||||
for (;;) {
|
||||
doWork();
|
||||
yield();
|
||||
}
|
||||
}
|
||||
|
||||
// CORRECT — delay(1) suspends the task for ≥1 ms, IDLE task runs, IDLE watchdog is fed
|
||||
void myTask(void*) {
|
||||
for (;;) {
|
||||
doWork();
|
||||
delay(1); // DO NOT REMOVE — lets IDLE(0) run and feeds its watchdog
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- Prefer blocking FreeRTOS primitives (`xQueueReceive`, `ulTaskNotifyTake`, `vTaskDelayUntil`) over `delay(1)` polling where precise timing or event-driven behaviour is needed.
|
||||
- **Watchdog note.** WLED disables the Task Watchdog by default (`WLED_WATCHDOG_TIMEOUT 0` in `wled.h`). When enabled, `esp_task_wdt_reset()` is called at the end of each `loop()` iteration. Long blocking operations inside `loop()` — such as OTA downloads or slow file I/O — must call `esp_task_wdt_reset()` periodically, or be restructured so the main loop is not blocked for longer than the configured timeout.
|
||||
|
||||
## Caveats and Pitfalls
|
||||
|
||||
- **LittleFS filenames**: File paths passed to `file.open()` must not exceed 255 bytes (`LFS_NAME_MAX`). Validate constructed paths (e.g., `/ledmap_` + segment name + `.json`) stay within this limit (assume standard configurations, like WLED_MAX_SEGNAME_LEN = 64).
|
||||
|
||||
- In C/C++, additive operators (`+`, `-`) have HIGHER precedence than shift operators (`<<`, `>>`). Therefore `x - edge0 << 8` correctly parses as `(x - edge0) << 8`. Do NOT flag this pattern as a precedence bug. When reviewing WLED fixed-point code or any C/C++ shift expressions, verify against cppreference before claiming precedence issues with mixed `-`/`+` and `<<`/`>>` expressions.
|
||||
|
||||
- **Float-to-unsigned conversion is undefined behavior when the value is out of range.** Converting a negative `float` directly to an unsigned integer type (`uint8_t`, `uint16_t`, …) is UB per the C++ standard — the Xtensa (ESP32) toolchain may silently wrap, but RISC-V (ESP32-C3/C5/C6/P4) can produce different results due to clamping. Cast through a signed integer first:
|
||||
```cpp
|
||||
// Undefined behavior — avoid:
|
||||
uint8_t angle = 40.74f * atan2f(dy, dx); // negative float → uint8_t is UB
|
||||
|
||||
// Correct — cast through int first:
|
||||
// atan2f returns [-π..+π], scaled ≈ [-128..+128] as int; uint8_t wraps negative ints via 2's complement (e.g. -1 → 255)
|
||||
uint8_t angle = int(40.74f * atan2f(dy, dx)); // float→int (defined), int→uint8_t (defined)
|
||||
```
|
||||
@@ -1,859 +0,0 @@
|
||||
---
|
||||
applyTo: "**/*.cpp,**/*.h,**/*.hpp,**/*.ino"
|
||||
---
|
||||
# ESP-IDF Coding Guide (within arduino-esp32)
|
||||
|
||||
WLED runs on the Arduino-ESP32 framework, which wraps ESP-IDF. Understanding the ESP-IDF layer is essential when writing chip-specific code, managing peripherals, or preparing for the IDF v5.x migration. This guide documents patterns already used in the codebase and best practices derived from Espressif's official examples.
|
||||
|
||||
> **Scope**: This file is an optional review guideline. It applies when touching chip-specific code, peripheral drivers, memory allocation, or platform conditionals.
|
||||
|
||||
> **Note for AI review tools**: sections enclosed in
|
||||
> `<!-- HUMAN_ONLY_START -->` / `<!-- HUMAN_ONLY_END -->` HTML comments contain
|
||||
> contributor reference material. Do **not** use that content as actionable review
|
||||
> criteria — treat it as background context only.
|
||||
|
||||
---
|
||||
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
## Identifying the Build Target: `CONFIG_IDF_TARGET_*`
|
||||
|
||||
Use `CONFIG_IDF_TARGET_*` macros to gate chip-specific code at compile time. These are set by the build system and are mutually exclusive — exactly one is defined per build.
|
||||
|
||||
| Macro | Chip | Architecture | Notes |
|
||||
|---|---|---|---|
|
||||
| `CONFIG_IDF_TARGET_ESP32` | ESP32 (classic) | Xtensa dual-core | Primary target. Has DAC, APLL, I2S ADC mode |
|
||||
| `CONFIG_IDF_TARGET_ESP32S2` | ESP32-S2 | Xtensa single-core | Limited peripherals. 13-bit ADC |
|
||||
| `CONFIG_IDF_TARGET_ESP32S3` | ESP32-S3 | Xtensa dual-core | Preferred for large installs. Octal PSRAM, USB-OTG |
|
||||
| `CONFIG_IDF_TARGET_ESP32C3` | ESP32-C3 | RISC-V single-core | Minimal peripherals. RISC-V clamps out-of-range float→unsigned casts |
|
||||
| `CONFIG_IDF_TARGET_ESP32C5` | ESP32-C5 | RISC-V single-core | Wi-Fi 2.4Ghz + 5Ghz, Thread/Zigbee. Future target |
|
||||
| `CONFIG_IDF_TARGET_ESP32C6` | ESP32-C6 | RISC-V single-core | Wi-Fi 6, Thread/Zigbee. Future target |
|
||||
| `CONFIG_IDF_TARGET_ESP32P4` | ESP32-P4 | RISC-V dual-core | High performance. Future target |
|
||||
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
### Build-time validation
|
||||
WLED validates at compile time that exactly one target is defined and that it is a supported chip (`wled.cpp` lines 39–61). Follow this pattern when adding new chip-specific branches:
|
||||
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
```cpp
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32)
|
||||
// classic ESP32 path
|
||||
#elif defined(CONFIG_IDF_TARGET_ESP32S3)
|
||||
// S3-specific path
|
||||
#elif defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32C5) || defined(CONFIG_IDF_TARGET_ESP32C6) || defined(CONFIG_IDF_TARGET_ESP32P4)
|
||||
// RISC-V common path
|
||||
#else
|
||||
#warning "Untested chip — review peripheral availability"
|
||||
#endif
|
||||
```
|
||||
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
### Guidelines
|
||||
|
||||
- **Always test on the actual chip** before claiming support. Simulators and cross-compilation can hide peripheral differences.
|
||||
- **Prefer `#elif` chains** over nested `#ifdef` for readability.
|
||||
- **Do not use `CONFIG_IDF_TARGET_*` for feature detection.** Use `SOC_*` capability macros instead (see next section). For example, use `SOC_I2S_SUPPORTS_ADC` instead of `CONFIG_IDF_TARGET_ESP32` to check for I2S ADC support.
|
||||
- When a feature must be disabled on certain chips, use explicit `static_assert()` or `#warning` directives so the build clearly reports what is missing.
|
||||
|
||||
---
|
||||
|
||||
## Hardware Capability Detection: `SOC_*` Macros
|
||||
|
||||
`SOC_*` macros (from `soc/soc_caps.h`) describe what the current chip supports. They are the correct way to check for peripheral features — they stay accurate when new chips are added, unlike `CONFIG_IDF_TARGET_*` checks.
|
||||
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
### Important `SOC_*` macros used in WLED
|
||||
|
||||
| Macro | Type | Used in | Purpose |
|
||||
|---|---|---|---|
|
||||
| `SOC_I2S_NUM` | `int` | `audio_source.h` | Number of I2S peripherals (1 or 2) |
|
||||
| `SOC_I2S_SUPPORTS_ADC` | `bool` | `usermods/audioreactive/audio_source.h` | I2S ADC sampling mode (ESP32 only) |
|
||||
| `SOC_I2S_SUPPORTS_APLL` | `bool` | `usermods/audioreactive/audio_source.h` | Audio PLL for precise sample rates |
|
||||
| `SOC_I2S_SUPPORTS_PDM_RX` | `bool` | `usermods/audioreactive/audio_source.h` | PDM microphone input |
|
||||
| `SOC_ADC_MAX_BITWIDTH` | `int` | `util.cpp` | ADC resolution (12 or 13 bits). Renamed to `CONFIG_SOC_ADC_RTC_MAX_BITWIDTH` in IDF v5 |
|
||||
| `SOC_ADC_CHANNEL_NUM(unit)` | `int` | `pin_manager.cpp` | ADC channels per unit |
|
||||
| `SOC_UART_NUM` | `int` | `dmx_input.cpp` | Number of UART peripherals |
|
||||
| `SOC_DRAM_LOW` / `SOC_DRAM_HIGH` | `addr` | `util.cpp` | DRAM address boundaries for validation |
|
||||
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
### Key pitfall
|
||||
`SOC_ADC_MAX_BITWIDTH` (ADC resolution 12 or 13 bits) was renamed to `CONFIG_SOC_ADC_RTC_MAX_BITWIDTH` in IDF v5.
|
||||
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
### Less commonly used but valuable
|
||||
|
||||
| Macro | Purpose |
|
||||
|---|---|
|
||||
| `SOC_RMT_TX_CANDIDATES_PER_GROUP` | Number of RMT TX channels (varies 2–8 by chip) |
|
||||
| `SOC_LEDC_CHANNEL_NUM` | Number of LEDC (PWM) channels |
|
||||
| `SOC_GPIO_PIN_COUNT` | Total GPIO pin count |
|
||||
| `SOC_DAC_SUPPORTED` | Whether the chip has a DAC (ESP32/S2 only) |
|
||||
| `SOC_SPIRAM_SUPPORTED` | Whether PSRAM interface exists |
|
||||
| `SOC_CPU_CORES_NUM` | Core count (1 or 2) — useful for task pinning decisions |
|
||||
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
|
||||
### Best practices
|
||||
|
||||
```cpp
|
||||
// Good: feature-based detection
|
||||
#if SOC_I2S_SUPPORTS_PDM_RX
|
||||
_config.mode = i2s_mode_t(I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_PDM);
|
||||
#else
|
||||
#warning "PDM microphones not supported on this chip"
|
||||
#endif
|
||||
|
||||
// Avoid: chip-name-based detection
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32S3)
|
||||
// happens to be correct today, but breaks when a new chip adds PDM support
|
||||
#endif
|
||||
```
|
||||
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
### PSRAM capability macros
|
||||
|
||||
For PSRAM presence, mode, and DMA access patterns:
|
||||
|
||||
| Macro | Meaning |
|
||||
|---|---|
|
||||
| `CONFIG_SPIRAM` / `BOARD_HAS_PSRAM` | PSRAM is present in the build configuration |
|
||||
| `CONFIG_SPIRAM_MODE_QUAD` | Quad-SPI PSRAM (standard, used on ESP32 classic and some S2/S3 boards) |
|
||||
| `CONFIG_SPIRAM_MODE_OCT` | Octal-SPI PSRAM — 8 data lines, DTR mode. Used on ESP32-S3 with octal PSRAM (e.g. N8R8 / N16R8 modules). Reserves GPIO 33–37 for the PSRAM bus — **do not allocate these pins** when this macro is defined. `wled.cpp` uses this to gate GPIO reservation. |
|
||||
| `CONFIG_SPIRAM_MODE_HEX` | Hex-SPI (16-line) PSRAM — future interface on ESP32-P4 running at up to 200 MHz. Used in `json.cpp` to report the PSRAM mode. |
|
||||
| `CONFIG_SOC_PSRAM_DMA_CAPABLE` | PSRAM buffers can be used with DMA (ESP32-S3 with octal PSRAM) |
|
||||
| `CONFIG_SOC_MEMSPI_FLASH_PSRAM_INDEPENDENT` | SPI flash and PSRAM on separate buses (no speed contention) |
|
||||
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
|
||||
#### Detecting octal/hex flash
|
||||
|
||||
On ESP32-S3 modules with OPI flash (e.g. N8R8 modules where the SPI flash itself runs in Octal-PI mode), the build system sets:
|
||||
|
||||
| Macro | Meaning |
|
||||
|---|---|
|
||||
| `CONFIG_ESPTOOLPY_FLASHMODE_OPI` | Octal-PI flash mode. On S3, implies GPIO 33–37 are used by the flash/PSRAM interface — the same GPIO block as octal PSRAM. `wled.cpp` uses `CONFIG_ESPTOOLPY_FLASHMODE_OPI \|\| (CONFIG_SPIRAM_MODE_OCT && BOARD_HAS_PSRAM)` to decide whether to reserve these GPIOs. `json.cpp` uses this to report the flash mode string as `"🚀OPI"`. |
|
||||
| `CONFIG_ESPTOOLPY_FLASHMODE_HEX` | Hex flash mode (ESP32-P4). Reported as `"🚀🚀HEX"` in `json.cpp`. |
|
||||
|
||||
**Pattern used in WLED** (from `wled.cpp`) to reserve the octal-bus GPIOs on S3:
|
||||
```cpp
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32S3)
|
||||
#if CONFIG_ESPTOOLPY_FLASHMODE_OPI || (CONFIG_SPIRAM_MODE_OCT && defined(BOARD_HAS_PSRAM))
|
||||
// S3: GPIO 33-37 are used by the octal PSRAM/flash bus
|
||||
managed_pin_type pins[] = { {33, true}, {34, true}, {35, true}, {36, true}, {37, true} };
|
||||
pinManager.allocateMultiplePins(pins, sizeof(pins)/sizeof(managed_pin_type), PinOwner::SPI_RAM);
|
||||
#endif
|
||||
#endif
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ESP-IDF Version Conditionals
|
||||
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
### Checking the IDF version
|
||||
|
||||
```cpp
|
||||
#include <esp_idf_version.h>
|
||||
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)
|
||||
// IDF v5+ code path
|
||||
#elif ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0)
|
||||
// IDF v4.4+ code path
|
||||
#else
|
||||
// Legacy IDF v3/v4.x path
|
||||
#endif
|
||||
```
|
||||
|
||||
### Key ESP-IDF version thresholds for WLED
|
||||
|
||||
| Version | What changed |
|
||||
|---|---|
|
||||
| **4.0.0** | Filesystem API (`SPIFFS`/`LittleFS`), GPIO driver overhaul |
|
||||
| **4.2.0** | ADC/GPIO API updates; `esp_adc_cal` introduced |
|
||||
| **4.4.0** | I2S driver refactored (legacy API remains); `adc_deprecated.h` headers appear for newer targets |
|
||||
| **4.4.4–4.4.8** | Known I2S channel-swap regression on ESP32 (workaround in `audio_source.h`) |
|
||||
| **5.0.0** | **Major breaking changes** — RMT, I2S, ADC, SPI flash APIs replaced (see migration section) |
|
||||
| **5.1.0** | Matter protocol support; new `esp_flash` API stable |
|
||||
| **5.3+** | arduino-esp32 v3.x compatibility; C6/P4 support |
|
||||
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
### Guidelines
|
||||
|
||||
- When adding a version guard, **always include a comment** explaining *what* changed and *why* the guard is needed.
|
||||
- Avoid version ranges that silently break — prefer `>=` over exact version matches.
|
||||
- Known regressions should use explicit range guards:
|
||||
```cpp
|
||||
// IDF 4.4.4–4.4.8 swapped I2S left/right channels (fixed in 4.4.9)
|
||||
#if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 4)) && \
|
||||
(ESP_IDF_VERSION <= ESP_IDF_VERSION_VAL(4, 4, 8))
|
||||
#define I2S_CHANNELS_SWAPPED
|
||||
#endif
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Migrating from ESP-IDF v4.4.x to v5.x
|
||||
|
||||
The jump from IDF v4.4 (arduino-esp32 v2.x) to IDF v5.x (arduino-esp32 v3.x) is the largest API break in ESP-IDF history. This section documents the critical changes and recommended migration patterns based on the upstream WLED `V5-C6` branch (`https://github.com/wled/WLED/tree/V5-C6`). Note: WLED has not yet migrated to IDF v5 — these patterns prepare for the future migration.
|
||||
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
### Compiler changes
|
||||
|
||||
IDF v5.x ships a much newer GCC toolchain. Key versions:
|
||||
|
||||
| ESP-IDF | GCC | C++ default | Notes |
|
||||
|---|---|---|---|
|
||||
| 4.4.x (current) | **8.4.0** | C++17 (gnu++17) | Xtensa + RISC-V |
|
||||
| 5.1–5.3 | **13.2** | C++20 (gnu++2b) | Significant warning changes |
|
||||
| 5.4–5.5 | **14.2** | C++23 (gnu++2b) | Latest; stricter diagnostics |
|
||||
|
||||
Notable behavioral differences:
|
||||
|
||||
| Change | Impact | Action |
|
||||
|---|---|---|
|
||||
| Stricter `-Werror=enum-conversion` | Implicit int-to-enum casts now error | Use explicit `static_cast<>` or typed enums |
|
||||
| C++20/23 features available | `consteval`, `concepts`, `std::span`, `std::expected` | Use judiciously — ESP8266 builds still require GCC 10.x with C++17 |
|
||||
| `-Wdeprecated-declarations` enforced | Deprecated API calls become warnings/errors | Migrate to new APIs (see below) |
|
||||
| `-Wdangling-reference` (GCC 13+) | Warns when a reference binds to a temporary that will be destroyed | Fix the lifetime issue; do not suppress the warning |
|
||||
| `-fno-common` default (GCC 12+) | Duplicate tentative definitions across translation units cause linker errors | Use `extern` declarations in headers, define in exactly one `.cpp` |
|
||||
| RISC-V codegen improvements | C3/C6/P4 benefit from better register allocation | No action needed — automatic |
|
||||
|
||||
### C++ language features: GCC 8 → GCC 14
|
||||
|
||||
The jump from GCC 8.4 to GCC 14.2 spans six major compiler releases. This section lists features that become available and patterns that need updating.
|
||||
|
||||
#### Features safe to use after migration
|
||||
|
||||
These work in GCC 13+/14+ but **not** in GCC 8.4. Guard with `#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)` if the code must compile on both IDF v4 and v5.
|
||||
|
||||
| Feature | Standard | Example | Benefit |
|
||||
|---|---|---|---|
|
||||
| Designated initializers (C++20) | C++20 | `gpio_config_t cfg = { .mode = GPIO_MODE_OUTPUT };` | Already used as a GNU extension in GCC 8; becomes standard and portable in C++20 |
|
||||
| `[[likely]]` / `[[unlikely]]` | C++20 | `if (err != ESP_OK) [[unlikely]] { ... }` | Hints for branch prediction; useful in hot paths |
|
||||
| `[[nodiscard("reason")]]` | C++20 | `[[nodiscard("leak if ignored")]] void* allocBuffer();` | Enforces checking return values — helpful for `esp_err_t` wrappers |
|
||||
| `std::span<T>` | C++20 | `void process(std::span<uint8_t> buf)` | Safe, non-owning view of contiguous memory — replaces raw pointer + length pairs |
|
||||
| `consteval` | C++20 | `consteval uint32_t packColor(...)` | Guarantees compile-time evaluation; useful for color constants |
|
||||
| `constinit` | C++20 | `constinit static int counter = 0;` | Prevents static initialization order fiasco |
|
||||
| Concepts / `requires` | C++20 | `template<typename T> requires std::integral<T>` | Clearer constraints than SFINAE; improves error messages |
|
||||
| Three-way comparison (`<=>`) | C++20 | `auto operator<=>(const Version&) const = default;` | Less boilerplate for comparable types |
|
||||
| `std::bit_cast` | C++20 | `float f = std::bit_cast<float>(uint32_val);` | Type-safe reinterpretation — replaces `memcpy` or `union` tricks |
|
||||
| `if consteval` | C++23 | `if consteval { /* compile-time */ } else { /* runtime */ }` | Cleaner than `std::is_constant_evaluated()` |
|
||||
| `std::expected<T, E>` | C++23 | `std::expected<int, esp_err_t> readSensor()` | Monadic error handling — cleaner than returning error codes |
|
||||
| `std::to_underlying` | C++23 | `auto val = std::to_underlying(myEnum);` | Replaces `static_cast<int>(myEnum)` |
|
||||
|
||||
#### Features already available in GCC 8 (C++17)
|
||||
|
||||
These work on both IDF v4.4 and v5.x — prefer them now:
|
||||
|
||||
| Feature | Example | Notes |
|
||||
|---|---|---|
|
||||
| `if constexpr` | `if constexpr (sizeof(T) == 4) { ... }` | Compile-time branching; already used in WLED |
|
||||
| `std::optional<T>` | `std::optional<uint8_t> pin;` | Nullable value without sentinel values like `-1` |
|
||||
| `std::string_view` | `void log(std::string_view msg)` | Non-owning, non-allocating string reference |
|
||||
| Structured bindings | `auto [err, value] = readSensor();` | Useful with `std::pair` / `std::tuple` returns |
|
||||
| Fold expressions | `(addSegment(args), ...);` | Variadic template expansion |
|
||||
| Inline variables | `inline constexpr int MAX_PINS = 50;` | Avoids ODR issues with header-defined constants |
|
||||
| `[[maybe_unused]]` | `[[maybe_unused]] int debug_only = 0;` | Suppresses unused-variable warnings cleanly |
|
||||
| `[[fallthrough]]` | `case 1: doA(); [[fallthrough]]; case 2:` | Documents intentional switch fallthrough |
|
||||
| Nested namespaces | `namespace wled::audio { }` | Shorter than nested `namespace` blocks |
|
||||
|
||||
#### Patterns that break or change behavior
|
||||
|
||||
| Pattern | GCC 8 behavior | GCC 14 behavior | Fix |
|
||||
|---|---|---|---|
|
||||
| `int x; enum E e = x;` | Warning (often ignored) | Error with `-Werror=enum-conversion` | `E e = static_cast<E>(x);` |
|
||||
| `int g;` in two `.cpp` files | Both compile, linker merges (tentative definition) | Error: multiple definitions (`-fno-common`) | `extern int g;` in header, `int g;` in one `.cpp` |
|
||||
| `const char* ref = std::string(...).c_str();` | Silent dangling pointer | Warning (`-Wdangling-reference`) | Extend lifetime: store the `std::string` in a local variable |
|
||||
| `register int x;` | Accepted (ignored) | Warning or error (`register` removed in C++17) | Remove `register` keyword |
|
||||
| Narrowing in aggregate init | Warning | Error | Use explicit cast or wider type |
|
||||
| Implicit `this` capture in lambdas | Accepted in `[=]` | Deprecated warning; error in C++20 mode | Use `[=, this]` or `[&]` |
|
||||
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
#### Recommendations
|
||||
|
||||
- **Do not raise the minimum C++ standard yet.** WLED must still build on IDF v4.4 (GCC 8.4, C++17). Use `#if __cplusplus > 201703L` to gate C++20 features.
|
||||
- **Mark intentional fallthrough** with `[[fallthrough]]` — GCC 14 warns on unmarked fallthrough by default.
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
- **Prefer `std::optional` over sentinel values** (e.g., `-1` for "no pin") in new code — it works on both compilers.
|
||||
- **Use `std::string_view`** for read-only string parameters instead of `const char*` or `const String&` — zero-copy and works on GCC 8+.
|
||||
- **Avoid raw `union` type punning** — prefer `memcpy` (GCC 8) or `std::bit_cast` (GCC 13+) for strict-aliasing safety.
|
||||
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
### Deprecated and removed APIs
|
||||
|
||||
#### RMT (Remote Control Transceiver)
|
||||
|
||||
The legacy `rmt_*` functions are removed in IDF v5. Do not introduce new legacy RMT calls.
|
||||
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
The new API is channel-based:
|
||||
|
||||
| IDF v4 (legacy) | IDF v5 (new) | Notes |
|
||||
|---|---|---|
|
||||
| `rmt_config()` + `rmt_driver_install()` | `rmt_new_tx_channel()` / `rmt_new_rx_channel()` | Channels are now objects |
|
||||
| `rmt_write_items()` | `rmt_transmit()` with encoder | Requires `rmt_encoder_t` |
|
||||
| `rmt_set_idle_level()` | Configure in channel config | Set at creation time |
|
||||
| `rmt_item32_t` | `rmt_symbol_word_t` | Different struct layout |
|
||||
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
**WLED impact**: NeoPixelBus LED output and IR receiver both use legacy RMT. The upstream `V5-C6` branch adds `-D WLED_USE_SHARED_RMT` and disables IR until the library is ported.
|
||||
|
||||
#### I2S (Inter-IC Sound)
|
||||
|
||||
Legacy `i2s_driver_install()` + `i2s_read()` API is deprecated. When touching audio source code, wrap legacy I2S init and reading in `#if ESP_IDF_VERSION_MAJOR < 5` / `#else`.
|
||||
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
The new API uses channel handles:
|
||||
| IDF v4 (legacy) | IDF v5 (new) | Notes |
|
||||
|---|---|---|
|
||||
| `i2s_driver_install()` | `i2s_channel_init_std_mode()` | Separate STD/PDM/TDM modes |
|
||||
| `i2s_set_pin()` | Pin config in `i2s_std_gpio_config_t` | Set at init time |
|
||||
| `i2s_read()` | `i2s_channel_read()` | Uses channel handle |
|
||||
| `i2s_set_clk()` | `i2s_channel_reconfig_std_clk()` | Reconfigure running channel |
|
||||
| `i2s_config_t` | `i2s_std_config_t` | Separate config for each mode |
|
||||
|
||||
**Migration pattern** (from Espressif examples):
|
||||
```cpp
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)
|
||||
#include "driver/i2s_std.h"
|
||||
i2s_chan_handle_t rx_handle;
|
||||
i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_0, I2S_ROLE_MASTER);
|
||||
i2s_new_channel(&chan_cfg, NULL, &rx_handle);
|
||||
|
||||
i2s_std_config_t std_cfg = {
|
||||
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(22050),
|
||||
.slot_cfg = I2S_STD_MSB_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_32BIT, I2S_SLOT_MODE_MONO),
|
||||
.gpio_cfg = { .din = GPIO_NUM_32, .mclk = I2S_GPIO_UNUSED, ... },
|
||||
};
|
||||
i2s_channel_init_std_mode(rx_handle, &std_cfg);
|
||||
i2s_channel_enable(rx_handle);
|
||||
#else
|
||||
// Legacy i2s_driver_install() path
|
||||
#endif
|
||||
```
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
**WLED impact**: The audioreactive usermod (`audio_source.h`) heavily uses legacy I2S. Migration requires rewriting the `I2SSource` class for channel-based API.
|
||||
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
#### ADC (Analog-to-Digital Converter)
|
||||
|
||||
Legacy `adc1_get_raw()` and `esp_adc_cal_*` are deprecated:
|
||||
|
||||
| IDF v4 (legacy) | IDF v5 (new) | Notes |
|
||||
|---|---|---|
|
||||
| `adc1_config_width()` + `adc1_get_raw()` | `adc_oneshot_new_unit()` + `adc_oneshot_read()` | Object-based API |
|
||||
| `esp_adc_cal_characterize()` | `adc_cali_create_scheme_*()` | Calibration is now scheme-based |
|
||||
| `adc_continuous_*` (old) | `adc_continuous_*` (restructured) | Config struct changes |
|
||||
|
||||
#### SPI Flash
|
||||
|
||||
| IDF v4 (legacy) | IDF v5 (new) |
|
||||
|---|---|
|
||||
| `spi_flash_read()` | `esp_flash_read()` |
|
||||
| `spi_flash_write()` | `esp_flash_write()` |
|
||||
| `spi_flash_erase_range()` | `esp_flash_erase_region()` |
|
||||
|
||||
WLED already has a compatibility shim in `ota_update.cpp` that maps old names to new ones.
|
||||
|
||||
#### GPIO
|
||||
|
||||
| IDF v4 (legacy) | IDF v5 (recommended) |
|
||||
|---|---|
|
||||
| `gpio_pad_select_gpio()` | `esp_rom_gpio_pad_select_gpio()` (or use `gpio_config()`) |
|
||||
| `gpio_set_direction()` + `gpio_set_pull_mode()` | `gpio_config()` with `gpio_config_t` struct |
|
||||
|
||||
### Features disabled in IDF v5 builds
|
||||
|
||||
The upstream `V5-C6` branch explicitly disables features with incompatible library dependencies:
|
||||
|
||||
```ini
|
||||
# platformio.ini [esp32_idf_V5]
|
||||
-D WLED_DISABLE_INFRARED # IR library uses legacy RMT
|
||||
-D WLED_DISABLE_MQTT # AsyncMqttClient incompatible with IDF v5
|
||||
-D ESP32_ARDUINO_NO_RGB_BUILTIN # Prevents RMT driver conflict with built-in LED
|
||||
-D WLED_USE_SHARED_RMT # Use new shared RMT driver for NeoPixel output
|
||||
```
|
||||
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
### Migration checklist for new code
|
||||
|
||||
1. **Never use a removed API without a version guard.** Always provide both old and new paths, or disable the feature on IDF v5.
|
||||
2. **Test on both IDF v4.4 and v5.x builds** if the code must be backward-compatible.
|
||||
3. **Prefer the newer API** when writing new code — wrap the old API in an `#else` block.
|
||||
4. **Mark migration TODOs** with `// TODO(idf5):` so they are easy to find later.
|
||||
|
||||
---
|
||||
|
||||
## Memory Management: `heap_caps_*` Best Practices
|
||||
|
||||
ESP32 has multiple memory regions with different capabilities. Using the right allocator is critical for performance and stability.
|
||||
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
### Memory regions
|
||||
|
||||
| Region | Flag | Speed | DMA | Size | Use for |
|
||||
|---|---|---|---|---|---|
|
||||
| DRAM | `MALLOC_CAP_INTERNAL \| MALLOC_CAP_8BIT` | Fast | Yes (ESP32) | 200–320 KB | Hot-path buffers, task stacks, small allocations |
|
||||
| IRAM | `MALLOC_CAP_EXEC` | Fastest | No | 32–128 KB | Code (automatic via `IRAM_ATTR`) |
|
||||
| PSRAM (SPIRAM) | `MALLOC_CAP_SPIRAM \| MALLOC_CAP_8BIT` | Slower | Chip-dependent | 2–16 MB | Large buffers, JSON documents, image data |
|
||||
| RTC RAM | `MALLOC_CAP_RTCRAM` | Moderate | No | 8 KB | Data surviving deep sleep; small persistent buffers |
|
||||
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
|
||||
### WLED allocation wrappers
|
||||
|
||||
WLED provides convenience wrappers with automatic fallback. **Always prefer these over raw `heap_caps_*` calls**:
|
||||
|
||||
| Function | Allocation preference | Use case |
|
||||
|---|---|---|
|
||||
| `d_malloc(size)` | RTC → DRAM → PSRAM | General-purpose; prefers fast memory |
|
||||
| `d_calloc(n, size)` | Same as `d_malloc`, zero-initialized | Arrays, structs |
|
||||
| `p_malloc(size)` | PSRAM → DRAM | Large buffers; prefers abundant memory |
|
||||
| `p_calloc(n, size)` | Same as `p_malloc`, zero-initialized | Large arrays |
|
||||
| `d_malloc_only(size)` | RTC → DRAM (no PSRAM fallback) | DMA buffers, time-critical data |
|
||||
|
||||
### PSRAM guidelines
|
||||
|
||||
- **Check availability**: test availability with `psramFound() && ESP.getPsramSize() > 0` before assuming PSRAM is present. Never rely on `BOARD_HAS_PSRAM`only.
|
||||
- **DMA compatibility**: on ESP32 (classic), PSRAM buffers are **not DMA-capable** — use `d_malloc_only()` to allocate DMA buffers in DRAM only. On ESP32-S3 with octal PSRAM (`CONFIG_SPIRAM_MODE_OCT`), PSRAM buffers *can* be used with DMA when `CONFIG_SOC_PSRAM_DMA_CAPABLE` is defined.
|
||||
- **JSON documents**: use the `PSRAMDynamicJsonDocument` allocator (defined in `wled.h`) to put large JSON documents in PSRAM:
|
||||
```cpp
|
||||
PSRAMDynamicJsonDocument doc(16384); // allocated in PSRAM if available
|
||||
```
|
||||
- **Fragmentation**: PSRAM allocations fragment less than DRAM because the region is larger. But avoid mixing small and large allocations in PSRAM — small allocations waste the MMU page granularity.
|
||||
- **Heap validation**: use `d_measureHeap()` and `d_measureContiguousFreeHeap()` to monitor remaining DRAM. Allocations that would drop free DRAM below `MIN_HEAP_SIZE` should go to PSRAM instead.
|
||||
- **Performance**: Keep hot-path data in DRAM. Prefer PSRAM for capacity-oriented buffers and monitor contiguous DRAM headroom.
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
PSRAM access is up to 15× slower than DRAM on ESP32, 3–10× slower than DRAM on ESP32-S3/-S2 with quad-SPI bus. On ESP32-S3 with octal PSRAM (`CONFIG_SPIRAM_MODE_OCT`), the penalty is smaller (~2×) because the 8-line DTR bus can transfer 8 bits in parallel at 80 MHz (120 MHz is possible with CONFIG_SPIRAM_SPEED_120M, which requires enabling experimental ESP-IDF features). On ESP32-P4 with hex PSRAM (`CONFIG_SPIRAM_MODE_HEX`), the 16-line bus runs at 200 MHz which brings it on-par with DRAM. Keep hot-path data in DRAM regardless, but consider that ESP32 often crashes when the largest DRAM chunk gets below 10 KB.
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
### Pattern: preference-based allocation
|
||||
|
||||
When you need a buffer that works on boards with or without PSRAM:
|
||||
|
||||
```cpp
|
||||
// Prefer PSRAM for large buffers, fall back to DRAM
|
||||
uint8_t* buf = (uint8_t*)heap_caps_malloc_prefer(bufSize, 2,
|
||||
MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT, // first choice: PSRAM
|
||||
MALLOC_CAP_DEFAULT); // fallback: any available
|
||||
// Or simply:
|
||||
uint8_t* buf = (uint8_t*)p_malloc(bufSize);
|
||||
```
|
||||
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
---
|
||||
|
||||
## I2S Audio: Best Practices
|
||||
|
||||
The audioreactive usermod uses I2S for microphone input. Key patterns:
|
||||
|
||||
### Port selection
|
||||
|
||||
```cpp
|
||||
constexpr i2s_port_t AR_I2S_PORT = I2S_NUM_0;
|
||||
// I2S_NUM_1 has limitations: no MCLK routing, no ADC support, no PDM support
|
||||
```
|
||||
|
||||
Always use `I2S_NUM_0` unless you have a specific reason and have verified support on all target chips.
|
||||
|
||||
### DMA buffer tuning
|
||||
|
||||
DMA buffer size controls latency vs. reliability:
|
||||
|
||||
| Scenario | `dma_buf_count` | `dma_buf_len` | Latency | Notes |
|
||||
|---|---|---|---|---|
|
||||
| With HUB75 matrix | 18 | 128 | ~100 ms | Higher count prevents I2S starvation during matrix DMA |
|
||||
| Without PSRAM | 24 | 128 | ~140 ms | More buffers compensate for slower interrupt response |
|
||||
| Default | 8 | 128 | ~46 ms | Acceptable for most setups |
|
||||
|
||||
### Interrupt priority
|
||||
|
||||
Choose interrupt priority based on coexistence with other drivers:
|
||||
|
||||
```cpp
|
||||
#ifdef WLED_ENABLE_HUB75MATRIX
|
||||
.intr_alloc_flags = ESP_INTR_FLAG_IRAM | ESP_INTR_FLAG_LEVEL1, // level 1 (lowest) to avoid starving HUB75
|
||||
#else
|
||||
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL2 | ESP_INTR_FLAG_LEVEL3, // accept level 2 or 3 (allocator picks available)
|
||||
#endif
|
||||
```
|
||||
|
||||
### APLL (Audio PLL) usage
|
||||
|
||||
The ESP32 has an audio PLL for precise sample rates. Rules:
|
||||
|
||||
- Enable APLL when an MCLK pin is provided and precision matters.
|
||||
- **Disable APLL** when Ethernet or HUB75 is active — they also use the APLL.
|
||||
- APLL is broken on ESP32 revision 0 silicon.
|
||||
- Not all chips have APLL — gate with `SOC_I2S_SUPPORTS_APLL`.
|
||||
|
||||
```cpp
|
||||
#if !defined(SOC_I2S_SUPPORTS_APLL)
|
||||
_config.use_apll = false;
|
||||
#elif defined(WLED_USE_ETHERNET) || defined(WLED_ENABLE_HUB75MATRIX)
|
||||
_config.use_apll = false; // APLL conflict
|
||||
#endif
|
||||
```
|
||||
|
||||
### PDM microphone caveats
|
||||
|
||||
- Not supported on ESP32-C3 (`SOC_I2S_SUPPORTS_PDM_RX` not defined).
|
||||
- ESP32-S3 PDM has known issues: sample rate at 50% of expected, very low amplitude.
|
||||
- **16-bit data width**: Espressif's IDF documentation states that in PDM mode the data unit width is always 16 bits, regardless of the configured `bits_per_sample`.
|
||||
- See [espressif/esp-idf#8660](https://github.com/espressif/esp-idf/issues/8660) for the upstream issue.
|
||||
- **Flag `bits_per_sample = I2S_BITS_PER_SAMPLE_32BIT` in PDM mode** — this causes the S3 low-amplitude symptom.
|
||||
- No clock pin (`I2S_CKPIN = -1`) triggers PDM mode in WLED.
|
||||
|
||||
---
|
||||
|
||||
## HUB75 LED Matrix: Best Practices
|
||||
|
||||
WLED uses the `ESP32-HUB75-MatrixPanel-I2S-DMA` library for HUB75 matrix output.
|
||||
|
||||
### Chip-specific panel limits
|
||||
|
||||
```cpp
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32S3) && defined(BOARD_HAS_PSRAM)
|
||||
maxChainLength = 6; // S3 + PSRAM: up to 6 panels (DMA-capable PSRAM)
|
||||
#elif defined(CONFIG_IDF_TARGET_ESP32S2)
|
||||
maxChainLength = 2; // S2: limited DMA channels
|
||||
#else
|
||||
maxChainLength = 4; // Classic ESP32: default
|
||||
#endif
|
||||
```
|
||||
|
||||
### Color depth vs. pixel count
|
||||
|
||||
The driver dynamically reduces color depth for larger displays to stay within DMA buffer limits:
|
||||
|
||||
| Pixel count | Color depth | Bits per pixel |
|
||||
|---|---|---|
|
||||
| ≤ `MAX_PIXELS_10BIT` | 10-bit (30-bit color) | High quality (experimental) |
|
||||
| ≤ `MAX_PIXELS_8BIT` | 8-bit (24-bit color) | Full quality |
|
||||
| ≤ `MAX_PIXELS_6BIT` | 6-bit (18-bit color) | Slight banding |
|
||||
| ≤ `MAX_PIXELS_4BIT` | 4-bit (12-bit color) | Visible banding |
|
||||
| larger | 3-bit (9-bit color) | Minimal color range |
|
||||
|
||||
### Resource conflicts
|
||||
|
||||
- **APLL**: HUB75 I2S DMA uses the APLL. Disable APLL in the audio I2S driver when HUB75 is active.
|
||||
- **I2S peripheral**: HUB75 uses `I2S_NUM_1` (or `I2S_NUM_0` on single-I2S chips). Audio must use the other port.
|
||||
- **Pin count**: HUB75 requires 13–14 GPIO pins. On ESP32-S2 this severely limits remaining GPIO.
|
||||
- **Reboot required**: on ESP32-S3, changing HUB75 driver options requires a full reboot — the I2S DMA cannot be reconfigured at runtime.
|
||||
|
||||
---
|
||||
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
## GPIO Best Practices
|
||||
|
||||
### Prefer `gpio_config()` over individual calls
|
||||
|
||||
```cpp
|
||||
// Preferred: single struct-based configuration
|
||||
gpio_config_t io_conf = {
|
||||
.pin_bit_mask = (1ULL << pin),
|
||||
.mode = GPIO_MODE_OUTPUT,
|
||||
.pull_up_en = GPIO_PULLUP_DISABLE,
|
||||
.pull_down_en = GPIO_PULLDOWN_DISABLE,
|
||||
.intr_type = GPIO_INTR_DISABLE,
|
||||
};
|
||||
gpio_config(&io_conf);
|
||||
|
||||
// Avoid: multiple separate calls (more error-prone, deprecated in IDF v5)
|
||||
gpio_set_direction(pin, GPIO_MODE_OUTPUT);
|
||||
gpio_set_pull_mode(pin, GPIO_FLOATING);
|
||||
```
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
|
||||
### Pin manager integration
|
||||
|
||||
Always allocate pins through WLED's `pinManager` before using GPIO APIs:
|
||||
|
||||
```cpp
|
||||
if (!pinManager.allocatePin(myPin, true, PinOwner::UM_MyUsermod)) {
|
||||
return; // pin in use by another module
|
||||
}
|
||||
// Now safe to configure
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Timer Best Practices
|
||||
|
||||
### Microsecond timing
|
||||
|
||||
For high-resolution timing, prefer `esp_timer_get_time()` (microsecond resolution, 64-bit) over `millis()` or `micros()`.
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
|
||||
```cpp
|
||||
#include <esp_timer.h>
|
||||
int64_t now_us = esp_timer_get_time(); // monotonic, not affected by NTP
|
||||
```
|
||||
|
||||
> **Note**: In arduino-esp32, both `millis()` and `micros()` are thin wrappers around `esp_timer_get_time()` — they share the same monotonic clock source. Prefer the direct call when you need the full 64-bit value or ISR-safe access without truncation:
|
||||
> ```cpp
|
||||
> // arduino-esp32 internals (cores/esp32/esp32-hal-misc.c):
|
||||
> // unsigned long micros() { return (unsigned long)(esp_timer_get_time()); }
|
||||
> // unsigned long millis() { return (unsigned long)(esp_timer_get_time() / 1000ULL); }
|
||||
> ```
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
### Periodic timers
|
||||
|
||||
For periodic tasks with sub-millisecond precision, use `esp_timer`:
|
||||
|
||||
```cpp
|
||||
esp_timer_handle_t timer;
|
||||
esp_timer_create_args_t args = {
|
||||
.callback = myCallback,
|
||||
.arg = nullptr,
|
||||
.dispatch_method = ESP_TIMER_TASK, // run in timer task (not ISR)
|
||||
.name = "my_timer",
|
||||
};
|
||||
esp_timer_create(&args, &timer);
|
||||
esp_timer_start_periodic(timer, 1000); // 1 ms period
|
||||
```
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
|
||||
Always prefer `ESP_TIMER_TASK` dispatch over `ESP_TIMER_ISR` unless you need ISR-level latency — ISR callbacks have severe restrictions (no logging, no heap allocation, no FreeRTOS API calls).
|
||||
|
||||
### Precision waiting: coarse delay then spin-poll
|
||||
|
||||
When waiting for a precise future deadline (e.g., FPS limiting, protocol timing), avoid spinning the entire duration — that wastes CPU and starves other tasks. Instead, yield to FreeRTOS while time allows, then spin only for the final window.
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
```cpp
|
||||
// Wait until 'target_us' (a micros() / esp_timer_get_time() timestamp)
|
||||
long time_to_wait = (long)(target_us - micros());
|
||||
// Coarse phase: yield to FreeRTOS while we have more than ~2 ms remaining.
|
||||
// vTaskDelay(1) suspends the task for one RTOS tick, letting other task run freely.
|
||||
while (time_to_wait > 2000) {
|
||||
vTaskDelay(1);
|
||||
time_to_wait = (long)(target_us - micros());
|
||||
}
|
||||
// Fine phase: busy-poll the last ≤2 ms for microsecond accuracy.
|
||||
// micros() wraps esp_timer_get_time() so this is low-overhead.
|
||||
while ((long)(target_us - micros()) > 0) { /* spin */ }
|
||||
```
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
|
||||
> The threshold (2000 µs as an example) should be at least one RTOS tick (default 1 ms on ESP32) plus some margin. A value of 1500–3000 µs works well in practice.
|
||||
|
||||
---
|
||||
|
||||
## ADC Best Practices
|
||||
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
### Version-aware ADC code
|
||||
|
||||
ADC is one of the most fragmented APIs across IDF versions:
|
||||
|
||||
```cpp
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)
|
||||
// IDF v5: oneshot driver
|
||||
#include "esp_adc/adc_oneshot.h"
|
||||
#include "esp_adc/adc_cali.h"
|
||||
adc_oneshot_unit_handle_t adc_handle;
|
||||
adc_oneshot_unit_init_cfg_t unit_cfg = { .unit_id = ADC_UNIT_1 };
|
||||
adc_oneshot_new_unit(&unit_cfg, &adc_handle);
|
||||
#else
|
||||
// IDF v4: legacy driver
|
||||
#include "driver/adc.h"
|
||||
#include "esp_adc_cal.h"
|
||||
adc1_config_width(ADC_WIDTH_BIT_12);
|
||||
int raw = adc1_get_raw(ADC1_CHANNEL_0);
|
||||
#endif
|
||||
```
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
|
||||
### Bit width portability
|
||||
|
||||
Not all chips have 12-bit ADC. `SOC_ADC_MAX_BITWIDTH` reports the maximum resolution (12 or 13 bits). Note that in IDF v5, this macro was renamed to `CONFIG_SOC_ADC_RTC_MAX_BITWIDTH`. Write version-aware guards:
|
||||
|
||||
```cpp
|
||||
// IDF v4: SOC_ADC_MAX_BITWIDTH IDF v5: CONFIG_SOC_ADC_RTC_MAX_BITWIDTH
|
||||
#if defined(CONFIG_SOC_ADC_RTC_MAX_BITWIDTH) // IDF v5+
|
||||
#define MY_ADC_MAX_BITWIDTH CONFIG_SOC_ADC_RTC_MAX_BITWIDTH
|
||||
#elif defined(SOC_ADC_MAX_BITWIDTH) // IDF v4
|
||||
#define MY_ADC_MAX_BITWIDTH SOC_ADC_MAX_BITWIDTH
|
||||
#else
|
||||
#define MY_ADC_MAX_BITWIDTH 12 // safe fallback
|
||||
#endif
|
||||
|
||||
#if MY_ADC_MAX_BITWIDTH == 13
|
||||
adc1_config_width(ADC_WIDTH_BIT_13); // ESP32-S2
|
||||
#else
|
||||
adc1_config_width(ADC_WIDTH_BIT_12); // ESP32, S3, C3, etc.
|
||||
#endif
|
||||
```
|
||||
|
||||
WLED's `util.cpp` uses the IDF v4 form (`SOC_ADC_MAX_BITWIDTH`) — this will need updating when the codebase migrates to IDF v5.
|
||||
|
||||
---
|
||||
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
## RMT Best Practices
|
||||
|
||||
### Current usage in WLED
|
||||
|
||||
RMT drives NeoPixel LED output (via NeoPixelBus) and IR receiver input. Both use the legacy API that is removed in IDF v5.
|
||||
|
||||
### Migration notes
|
||||
|
||||
- The upstream `V5-C6` branch uses `-D WLED_USE_SHARED_RMT` to switch to the new RMT driver for NeoPixel output.
|
||||
- IR is disabled on IDF v5 until the IR library is ported.
|
||||
- New chips (C6, P4) have different RMT channel counts — use `SOC_RMT_TX_CANDIDATES_PER_GROUP` to check availability.
|
||||
- The new RMT API requires an "encoder" object (`rmt_encoder_t`) to translate data formats — this is more flexible but requires more setup code.
|
||||
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
---
|
||||
|
||||
## Espressif Best Practices (from official examples)
|
||||
|
||||
### Error handling
|
||||
|
||||
Always check `esp_err_t` return values. Use `ESP_ERROR_CHECK()` in initialization code, but handle errors gracefully in runtime code.
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
```cpp
|
||||
// Initialization — crash early on failure
|
||||
ESP_ERROR_CHECK(i2s_driver_install(I2S_NUM_0, &config, 0, nullptr));
|
||||
|
||||
// Runtime — log and recover
|
||||
esp_err_t err = i2s_read(I2S_NUM_0, buf, len, &bytes_read, portMAX_DELAY);
|
||||
if (err != ESP_OK) {
|
||||
DEBUGSR_PRINTF("I2S read failed: %s\n", esp_err_to_name(err));
|
||||
return;
|
||||
}
|
||||
```
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
|
||||
For situations between these two extremes — where you want the `ESP_ERROR_CHECK` formatted log message (file, line, error name) but must not abort — use `ESP_ERROR_CHECK_WITHOUT_ABORT()`.
|
||||
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
```cpp
|
||||
// Logs in the same format as ESP_ERROR_CHECK, but returns the error code instead of aborting.
|
||||
// Useful for non-fatal driver calls where you want visibility without crashing.
|
||||
esp_err_t err = ESP_ERROR_CHECK_WITHOUT_ABORT(i2s_set_clk(AR_I2S_PORT, rate, bits, ch));
|
||||
if (err != ESP_OK) return; // handle as needed
|
||||
```
|
||||
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
### Logging
|
||||
|
||||
WLED uses its own logging macros — **not** `ESP_LOGx()`. For application-level code, always use the WLED macros defined in `wled.h`:
|
||||
|
||||
| Macro family | Defined in | Controlled by | Use for |
|
||||
|---|---|---|---|
|
||||
| `DEBUG_PRINT` / `DEBUG_PRINTLN` / `DEBUG_PRINTF` | `wled.h` | `WLED_DEBUG` build flag | Development/diagnostic output; compiled out in release builds |
|
||||
|
||||
All of these wrap `Serial` output through the `DEBUGOUT` / `DEBUGOUTLN` / `DEBUGOUTF` macros.
|
||||
|
||||
**Exception — low-level driver code**: When writing code that interacts directly with ESP-IDF APIs (e.g., I2S initialization, RMT setup), use `ESP_LOGx()` macros instead. They support tag-based filtering and compile-time log level control:
|
||||
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
```cpp
|
||||
static const char* TAG = "my_module";
|
||||
ESP_LOGI(TAG, "Initialized with %d buffers", count);
|
||||
ESP_LOGW(TAG, "PSRAM not available, falling back to DRAM");
|
||||
ESP_LOGE(TAG, "Failed to allocate %u bytes", size);
|
||||
```
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
### Task creation and pinning
|
||||
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
On dual-core chips (ESP32, S3, P4), pin latency-sensitive tasks to a specific core:
|
||||
|
||||
```cpp
|
||||
xTaskCreatePinnedToCore(
|
||||
audioTask, // function
|
||||
"audio", // name
|
||||
4096, // stack size
|
||||
nullptr, // parameter
|
||||
5, // priority (higher = more important)
|
||||
&audioTaskHandle, // handle
|
||||
0 // core ID (0 = protocol core, 1 = app core)
|
||||
);
|
||||
```
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
|
||||
Guidelines:
|
||||
- Pin network/protocol tasks to core 0 (where Wi-Fi runs).
|
||||
- Pin real-time tasks (audio, LED output) to core 1.
|
||||
- On single-core chips (S2, C3, C5, C6), only core 0 exists — pinning to core 1 will fail. Use `SOC_CPU_CORES_NUM > 1` guards or `tskNO_AFFINITY`.
|
||||
- Use `SOC_CPU_CORES_NUM` to conditionally pin tasks:
|
||||
```cpp
|
||||
#if SOC_CPU_CORES_NUM > 1
|
||||
xTaskCreatePinnedToCore(audioTask, "audio", 4096, nullptr, 5, &handle, 1);
|
||||
#else
|
||||
xTaskCreate(audioTask, "audio", 4096, nullptr, 5, &handle);
|
||||
#endif
|
||||
```
|
||||
|
||||
**Tip: use xTaskCreateUniversal()** - from arduino-esp32 - to avoid the conditional on `SOC_CPU_CORES_NUM`. This function has the same signature as ``xTaskCreatePinnedToCore()``, but automatically falls back to ``xTaskCreate()`` on single-core MCUs.
|
||||
|
||||
### `delay()`, `yield()`, and the IDLE task
|
||||
|
||||
FreeRTOS on ESP32 is **preemptive** — all tasks are scheduled by priority regardless of `yield()` calls. This is fundamentally different from ESP8266 cooperative multitasking.
|
||||
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
| Call | What it does | Reaches IDLE (priority 0)? |
|
||||
|---|---|---|
|
||||
| `delay(ms)` / `vTaskDelay(ticks)` | Suspends calling task; scheduler runs all other ready tasks | ✅ Yes |
|
||||
| `yield()` / `vTaskDelay(0)` | Hint to switch to tasks at **equal or higher** priority only | ❌ No |
|
||||
| `taskYIELD()` | Same as `vTaskDelay(0)` | ❌ No |
|
||||
| Blocking API (`xQueueReceive`, `ulTaskNotifyTake`, `vTaskDelayUntil`) | Suspends task until event or timeout; IDLE runs freely | ✅ Yes |
|
||||
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
|
||||
**`delay()` in `loopTask` is safe.** Arduino's `loop()` runs inside `loopTask`. Calling `delay()` suspends only `loopTask` — all other FreeRTOS tasks (Wi-Fi stack, audio FFT, LED DMA) continue uninterrupted on either core.
|
||||
|
||||
**`yield()` does not yield to IDLE.** Any task that loops with only `yield()` calls will starve the IDLE task, causing the IDLE watchdog to fire. Always use `delay(1)` (or a blocking FreeRTOS call) in tight task loops. Note: WLED redefines `yield()` as an empty macro on ESP32 WLEDMM_FASTPATH builds.
|
||||
|
||||
#### Why the IDLE task is not optional
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
|
||||
The FreeRTOS IDLE task (one per core on dual-core ESP32 and ESP32-S3; single instance on single-core chips) is not idle in the casual sense — it performs essential system housekeeping:
|
||||
|
||||
- **Frees deleted task memory**: when a task calls `vTaskDelete()`, the IDLE task reclaims its TCB and stack. Without IDLE running, deleted tasks leak memory permanently.
|
||||
- **Runs the idle hook**: when `configUSE_IDLE_HOOK = 1`, the IDLE task calls `vApplicationIdleHook()` on every iteration — some ESP-IDF components register low-priority background work here.
|
||||
- **Implements tickless idle / light sleep**: on battery-powered devices, IDLE is the entry point for low-power sleep. A permanently starved IDLE task disables light sleep entirely.
|
||||
- **Runs registered idle hooks**: ESP-IDF components register callbacks via `esp_register_freertos_idle_hook()` (e.g., Wi-Fi background maintenance, Bluetooth housekeeping). These only fire when IDLE runs.
|
||||
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
|
||||
In short: **starving IDLE corrupts memory cleanup, breaks background activities, disables low-power sleep, and prevents Wi-Fi/BT maintenance.** The IDLE watchdog panic is a symptom — the real damage happens before the watchdog fires.
|
||||
|
||||
### Watchdog management
|
||||
|
||||
Long-running operations may trigger the task watchdog. Feed it explicitly:
|
||||
|
||||
```cpp
|
||||
#include <esp_task_wdt.h>
|
||||
esp_task_wdt_reset(); // feed the watchdog in long loops
|
||||
```
|
||||
|
||||
For tasks that intentionally block for extended periods, consider subscribing/unsubscribing from the TWDT:
|
||||
|
||||
```cpp
|
||||
esp_task_wdt_delete(NULL); // remove current task from TWDT (IDF v4.4)
|
||||
// ... long blocking operation ...
|
||||
esp_task_wdt_add(NULL); // re-register
|
||||
```
|
||||
|
||||
> **IDF v5 note**: In IDF v5, `esp_task_wdt_add()` and `esp_task_wdt_delete()` require an explicit `TaskHandle_t`. Use `xTaskGetCurrentTaskHandle()` instead of `NULL`.
|
||||
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
---
|
||||
|
||||
## Quick Reference: IDF v4 → v5 API Mapping
|
||||
|
||||
| Component | IDF v4 Header | IDF v5 Header | Key Change |
|
||||
|---|---|---|---|
|
||||
| I2S | `driver/i2s.h` | `driver/i2s_std.h` | Channel-based API |
|
||||
| ADC (oneshot) | `driver/adc.h` | `esp_adc/adc_oneshot.h` | Unit/channel handles |
|
||||
| ADC (calibration) | `esp_adc_cal.h` | `esp_adc/adc_cali.h` | Scheme-based calibration |
|
||||
| RMT | `driver/rmt.h` | `driver/rmt_tx.h` / `rmt_rx.h` | Encoder-based transmit |
|
||||
| SPI Flash | `spi_flash.h` | `esp_flash.h` | `esp_flash_*` functions |
|
||||
| GPIO | `driver/gpio.h` | `driver/gpio.h` | `gpio_pad_select_gpio()` removed |
|
||||
| Timer | `driver/timer.h` | `driver/gptimer.h` | General-purpose timer handles |
|
||||
| PCNT | `driver/pcnt.h` | `driver/pulse_cnt.h` | Handle-based API |
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
@@ -1,58 +0,0 @@
|
||||
---
|
||||
applyTo: "**/*.{cpp,h,hpp,ino,js,htm,html,css,yml,yaml}"
|
||||
description: "WLED strict-mode security review: low-noise checklist."
|
||||
---
|
||||
|
||||
# WLED Security Review — Low Noise Mode
|
||||
|
||||
Use these code hardening rules for automated reviews with minimal false positives.
|
||||
|
||||
## WLED Constraints (apply to all rules)
|
||||
|
||||
- Assume firewall/DMZ/VPN deployment; focus on realistic LAN-local and supply-chain risks.
|
||||
- Do **not** require TLS/HTTPS as a baseline control.
|
||||
- Do **not** require authentication for standards-based UDP multicast/broadcast paths where authentication is not defined in the protocol specification.
|
||||
|
||||
> **Trust boundary model**: Apply input-validation rules **only at the first untrusted ingress point**
|
||||
> (HTTP/JSON API body or query string, WebSocket payload, UDP datagram, TCP read, serial command, ESP-NOW raw messages).
|
||||
> Values that have been validated and range-clamped at ingress are **trusted** for internal WLED
|
||||
> processing. Do not flag subsequent uses or internal copies of already-sanitized data.
|
||||
|
||||
## CRITICAL Rules
|
||||
|
||||
1. **No unchecked buffer copies** (`memcpy`, `memmove`, `strcpy`) in firmware paths when source buffer or size comes from an untrusted origin; prefer bounded alternatives (`strncpy`, `strlcpy`); require length validation before copying.
|
||||
2. **No user-controlled format strings** in `DEBUG_PRINTF*` and similar logging APIs.
|
||||
3. **Validate all untrusted external input** (HTTP/JSON/UDP/serial) before index/length/pin usage.
|
||||
4. **Auth required for state-changing control endpoints where feasible** (for example HTTP/JSON); do not flag protocol-defined unauthenticated UDP multicast/broadcast channels solely for missing auth.
|
||||
5. **No fail-open on parse/allocation errors** for config/state updates.
|
||||
6. **No DOM XSS sinks with untrusted data** (`innerHTML`, unsafe HTML insertion). Server-side generation of JavaScript property-assignment statements (as used in WLED's printSetForm* helpers) is exempt.
|
||||
7. **No dynamic code execution** (`eval`, `new Function`, string timers).
|
||||
8. **No hardcoded secrets/credentials/tokens/keys** in committed files.
|
||||
9. **No sensitive data in logs** (passwords, tokens, Wi-Fi secrets, auth headers).
|
||||
10. **No secret exposure in workflows/log output, or in LittleFS files other than `wsec.json`**.
|
||||
11. **No unsafe third-party GitHub Action pinning** (`@main`/`@master` disallowed).
|
||||
12. **No untrusted expression interpolation in workflow shell commands**.
|
||||
|
||||
## IMPORTANT Rules
|
||||
|
||||
13. Avoid potentially unbounded string/memory operations (`strcmp`, `strchr`, `strlen`, `sprintf`) in firmware paths; prefer bounded alternatives (`strnlen`, `strncmp`, `snprintf`).
|
||||
14. Check integer overflow risks in size/index arithmetic, but consider that unsigned wrap-around on small types might be intentional.
|
||||
15. Reject repeated heap allocation churn in hot render/effect loops.
|
||||
16. Avoid repeated `String` growth in hot paths; prefer bounded/pre-allocated buffers.
|
||||
17. Ensure UI validation is mirrored by firmware-side validation.
|
||||
18. Require strict origin checks for `postMessage` listeners.
|
||||
19. Disallow untrusted redirect/navigation targets.
|
||||
20. Prevent verbose error responses that leak internals.
|
||||
21. Review new dependencies for typosquatting and known vulnerability risk.
|
||||
22. Keep workflow `permissions` least-privilege.
|
||||
23. Verify new `WLED_ENABLE_*` / `WLED_DISABLE_*` names are valid known flags.
|
||||
24. New privileged behavior must not be enabled by insecure defaults; first-use default-credential change required where applicable.
|
||||
25. OTA paths (Update.begin(), Update.write()) must verify firmware integrity (checksum/hash); TLS not required.
|
||||
26. Flag xTaskCreate/xTaskCreatePinnedToCore tasks with insufficient stack for String/JSON use; flag MDNS.begin() / ArduinoOTA.setHostname() with unsanitized hostnames.
|
||||
27. Flag API/config serialization that exposes Wi-Fi/AP/MQTT password fields to unauthenticated clients.
|
||||
28. Treat fetched and config-derived strings as untrusted when inserting into the DOM; explicit sanitization required for HTML contexts.
|
||||
|
||||
## Reviewer Output Format
|
||||
|
||||
- Include severity, exact file and line, and one concrete fix direction.
|
||||
- Prioritize CRITICAL findings before IMPORTANT findings.
|
||||
@@ -1,227 +0,0 @@
|
||||
---
|
||||
applyTo: "**/*.{cpp,h,hpp,ino,js,htm,html,css,yml,yaml}"
|
||||
description: "WLED-focused security review guide based on OWASP Top 10 for embedded firmware and web UI."
|
||||
---
|
||||
|
||||
# WLED Security Review Standards (Embedded + Web UI)
|
||||
|
||||
Use this guide for AI-assisted code reviews in:
|
||||
- `wled00/`
|
||||
- `usermods/`
|
||||
- `.github/workflows/`
|
||||
|
||||
## WLED Constraints and Threat Model Assumptions
|
||||
|
||||
- Assume typical deployment behind a firewall/DMZ/VPN; prioritize LAN-local and supply-chain risks.
|
||||
- Do **not** require TLS/HTTPS as a baseline control for findings in this repo.
|
||||
- Do **not** require authentication for standards-based UDP multicast/broadcast protocols where auth is not part of the spec.
|
||||
- Do not propose mitigations that break protocol compliance just to add authentication.
|
||||
|
||||
### Trust Boundary Model
|
||||
|
||||
**Untrusted data** enters WLED only at the following explicit ingress points:
|
||||
- HTTP/JSON API request bodies and query parameters (e.g., `/json/*`, `/win`)
|
||||
- WebSocket message payloads
|
||||
- UDP datagrams (`parsePacket()` / `recvfrom()` and higher-level protocol wrappers)
|
||||
- TCP socket reads
|
||||
- Serial/UART input used as commands
|
||||
- ESP-NOW raw messages input
|
||||
|
||||
**Validation and range-clamping applied at the ingress point renders data trusted** for all subsequent use within the WLED core.
|
||||
|
||||
**Do not flag:**
|
||||
- Repeated bounds or range checks on a value that has already been validated and clamped at its ingress handler.
|
||||
- Internal WLED core logic that operates on values confirmed safe by the ingress layer.
|
||||
|
||||
If it is unclear whether a value has been sanitized upstream (e.g., passed through multiple function calls without a clear annotation), prefer asking for clarification over raising a false-positive finding.
|
||||
|
||||
### Locally-Stored Configuration Files (Robustness, not a primary trust boundary)
|
||||
|
||||
Files read from LittleFS (`presets.json`, `cfg.json`, `ledmap.json`, `ir.json`, etc.) are written only via privileged access (`/edit`) and are considered trusted in the threat model.
|
||||
However, parse them defensively (validate structure, clamp array sizes, handle missing keys gracefully) to avoid bootloops from filesystem corruption or accidental malformation.
|
||||
|
||||
## Severity
|
||||
|
||||
- **CRITICAL** — exploitable vulnerability; block merge.
|
||||
- **IMPORTANT** — meaningful risk; fix before or with merge when practical.
|
||||
- **SUGGESTION** — defense-in-depth; track for follow-up.
|
||||
|
||||
## Scope (WLED-relevant)
|
||||
|
||||
Prioritize:
|
||||
- C++ memory safety and input validation
|
||||
- Auth and access checks for state-changing HTTP/JSON APIs
|
||||
- XSS and DOM safety in `wled00/data/*`
|
||||
- Secrets handling (`wsec.json`) and secure logging
|
||||
- Dependency and GitHub Actions supply-chain hygiene
|
||||
- Fail-safe behavior on constrained devices
|
||||
|
||||
De-prioritize unless explicitly introduced by a PR:
|
||||
- SQL/NoSQL checks, JWT/OAuth flows, GraphQL-specific checks, generic backend framework checks not used by WLED.
|
||||
|
||||
## Firmware Security (C++, OWASP A01/A04/A05/A10)
|
||||
|
||||
### FW1: Unsafe buffer operations
|
||||
- **Severity**: CRITICAL
|
||||
- Flag `strcpy`, `sprintf`, unchecked memory access (`memcpy`, `memmove`, `memcmp`, `strcmp`, `strlen`), unchecked pointer arithmetic.
|
||||
- Require explicit bounds checks and length validation.
|
||||
- Prefer bounded alternatives for string operations (`strnlen`, `strncmp`, `strncpy`, `strlcpy`, `snprintf`).
|
||||
- Treat a finding against FW1 as **suggestion** only when the operation is provably bounded
|
||||
and both the destination capacity and copied/compared length are known safe.
|
||||
|
||||
### FW2: Format-string injection
|
||||
- **Severity**: CRITICAL
|
||||
- Do not pass untrusted input as a format string to `DEBUG_PRINTF*` or similar APIs.
|
||||
|
||||
### FW3: Integer overflow in length and offset math
|
||||
- **Severity**: IMPORTANT
|
||||
- Review `count * size`, index math, narrowing casts before allocations or copies.
|
||||
|
||||
### FW4: Unvalidated external input
|
||||
- **Severity**: CRITICAL
|
||||
- At each **untrusted ingress point** (see Trust Boundary Model above), validate and clamp values from HTTP/JSON/UDP/serial before use as lengths, indices, IDs, or pin references.
|
||||
- Do not flag repeated range checks on values that have already been validated at their ingress point.
|
||||
- In UDP handlers (`parsePacket()`, `read()`, and any lower-level socket wrappers), validate `packetSize` before buffer writes and clamp protocol-specific universe/channel ranges to valid limits.
|
||||
|
||||
### FW5: Missing auth checks on state-changing endpoints (where auth is feasible)
|
||||
- **Severity**: CRITICAL
|
||||
- HTTP/JSON and other control paths that support auth must enforce configured auth policy.
|
||||
- Do not flag the HTTP endpoint `/reset` as state-changing. This endpoint triggers a reboot, causing a short interruption without loss of user data.
|
||||
- Do not flag standards-based UDP multicast/broadcast paths solely for lacking authentication when authentication is not defined in the protocol specification.
|
||||
|
||||
### FW6: Fail-open behavior after parse or allocation errors
|
||||
- **Severity**: IMPORTANT
|
||||
- On error, reject update and preserve safe previous state.
|
||||
- Explicitly check parse status (`DeserializationError error = deserializeJson(...); if (error) return/reject;`) and avoid silently applying unsafe zero/default values to safety-relevant fields (for example LED count and pin assignment).
|
||||
|
||||
### FW7: Heap churn in hot paths
|
||||
- **Severity**: IMPORTANT
|
||||
- Avoid repeated dynamic allocation in render/effect loops; prefer pre-allocation and reuse.
|
||||
- Flag allocation patterns in loop and ISR-adjacent paths that can trigger fragmentation or timing instability.
|
||||
|
||||
### FW8: Unsafe use of `String` in performance-critical paths
|
||||
- **Severity**: IMPORTANT
|
||||
- In hot paths, avoid repeated `String` growth; reserve or use fixed buffers.
|
||||
- Flag repeated `String` concatenation inside loop-heavy or ISR-adjacent code.
|
||||
|
||||
### FW9: Unsafe feature flag names
|
||||
- **Severity**: IMPORTANT
|
||||
- Verify all new `WLED_ENABLE_*`/`WLED_DISABLE_*` names are valid known flags; typos silently alter build behavior.
|
||||
|
||||
### FW10: OTA integrity verification (without TLS requirement)
|
||||
- **Severity**: IMPORTANT
|
||||
- OTA update flows should validate firmware integrity using the checksum/hash/signature mechanism available in the firmware/platform implementation.
|
||||
- Do not require TLS/certificate pinning as a mandatory review criterion.
|
||||
- In OTA paths (`Update.begin()`, `Update.write()`, and related flows), flag flashing without integrity verification.
|
||||
|
||||
### FW11: FreeRTOS task stack and recursion safety
|
||||
- **Severity**: IMPORTANT
|
||||
- In `xTaskCreate`/`xTaskCreatePinnedToCore` tasks that process `String`/JSON-heavy data, verify stack-size sufficiency and avoid unbounded recursion.
|
||||
|
||||
### FW12: mDNS and hostname sanitization
|
||||
- **Severity**: IMPORTANT
|
||||
- For `MDNS.begin()`, `MDNS.addService()`, and `ArduinoOTA.setHostname()`, ensure user-provided hostnames are RFC-compliant (letters/digits/hyphen, no leading/trailing hyphen) and clamped to 63 characters.
|
||||
|
||||
### FW13: Outbound URL validation (no HTTPS requirement)
|
||||
- **Severity**: SUGGESTION
|
||||
- When using user-provided URL strings with `HTTPClient.begin()`/equivalent, validate scheme/format and constrain host targets (allowlist or equivalent policy).
|
||||
- Do not require HTTPS/TLS as a baseline review rule.
|
||||
|
||||
### FW14: Optional unicast UDP source filtering
|
||||
- **Severity**: SUGGESTION
|
||||
- For unicast UDP receive paths, prefer optional user-configurable source filtering.
|
||||
- Do not require this for multicast/broadcast protocol flows.
|
||||
|
||||
## Web UI Security (`wled00/data/*`, OWASP A01/A02/A05)
|
||||
|
||||
### WEB1: DOM XSS through `innerHTML`
|
||||
- **Severity**: CRITICAL
|
||||
- Prefer `textContent`; if HTML is required, sanitize trusted content path explicitly.
|
||||
|
||||
### WEB2: Dynamic code execution
|
||||
- **Severity**: CRITICAL
|
||||
- Reject `eval`, `new Function`, and string-based timer execution.
|
||||
|
||||
### WEB3: `postMessage` without origin validation
|
||||
- **Severity**: IMPORTANT
|
||||
- Require strict origin allowlist checks before processing payloads.
|
||||
|
||||
### WEB4: Unsafe redirects/navigation
|
||||
- **Severity**: IMPORTANT
|
||||
- Do not navigate directly from untrusted query/input without relative-path or allowlist checks.
|
||||
|
||||
### WEB5: Client-only validation
|
||||
- **Severity**: IMPORTANT
|
||||
- UI validation is not sufficient; equivalent firmware-side validation is required.
|
||||
|
||||
### WEB6: Direct DOM insertion from fetched/config data
|
||||
- **Severity**: IMPORTANT
|
||||
- Treat fetched and config-derived strings as untrusted unless proven otherwise.
|
||||
|
||||
### WEB7: CSRF checks for state-changing HTTP routes (advisory)
|
||||
- **Severity**: SUGGESTION
|
||||
- For state-changing HTTP routes (for example `/json/state`, `/win`), prefer `Origin`/`Referer` header validation as low-cost defense-in-depth for deployments that are not directly internet-exposed.
|
||||
- Treat this as advisory only, since some legitimate clients may omit these headers.
|
||||
|
||||
## Secrets and Logging (OWASP A04/A09/A10)
|
||||
|
||||
### SEC1: Hardcoded secrets and credentials
|
||||
- **Severity**: CRITICAL
|
||||
- Reject committed API keys, passwords, tokens, private keys, or test backdoors with potential security impact.
|
||||
|
||||
### SEC2: Sensitive values in logs
|
||||
- **Severity**: CRITICAL
|
||||
- Do not log passwords, tokens, Wi-Fi keys, auth headers, or full sensitive payloads.
|
||||
|
||||
### SEC3: Insecure defaults
|
||||
- **Severity**: IMPORTANT
|
||||
- Reject new default credentials or insecure auto-enable behavior for privileged functions.
|
||||
- For setup/onboarding flows, require first-change behavior for default credentials where applicable.
|
||||
|
||||
### SEC4: Overly detailed error responses
|
||||
- **Severity**: IMPORTANT
|
||||
- Avoid exposing stack traces or internal details to API/UI consumers.
|
||||
|
||||
### SEC5: Credential exposure in API/config responses
|
||||
- **Severity**: IMPORTANT
|
||||
- Flag API/config serialization that exposes password-like fields (for example Wi-Fi/AP/MQTT passwords) to unauthenticated or untrusted clients.
|
||||
|
||||
### SEC6: Security-relevant event logging coverage
|
||||
- **Severity**: SUGGESTION
|
||||
- Prefer explicit logging for auth failures, OTA attempts, config resets, and AP activation events, without logging secret values.
|
||||
|
||||
## Supply Chain and CI/CD (OWASP A03/A08)
|
||||
|
||||
### SC1: New dependency risk
|
||||
- **Severity**: IMPORTANT
|
||||
- Review new npm/pip/PlatformIO dependencies for legitimacy, pinning, and known vulnerabilities.
|
||||
|
||||
### SC2: Workflow permission hardening regressions
|
||||
- **Severity**: IMPORTANT
|
||||
- Check for broad `permissions`, unpinned third-party actions, or unsafe secret exposure.
|
||||
- Flag mutable third-party action refs (`@main`, `@master`, broad tags) where SHA pinning is expected by project policy.
|
||||
- Flag overly broad permissions such as `write-all` without clear need.
|
||||
|
||||
### SC3: Script injection in workflows
|
||||
- **Severity**: IMPORTANT
|
||||
- Avoid direct interpolation of untrusted `${{ github.event.* }}` values in `run` commands.
|
||||
|
||||
## Reviewer Checklist
|
||||
|
||||
- [ ] No new memory-safety hazards (bounds, overflow, unsafe copies/format strings)
|
||||
- [ ] External input is validated and range-clamped at ingress points (HTTP/JSON, WebSocket, UDP, TCP, serial, ESP-NOW)
|
||||
- [ ] State-changing API paths enforce auth policy
|
||||
- [ ] OTA paths enforce integrity verification (without requiring TLS baseline)
|
||||
- [ ] Suggested rule patterns are checked where relevant (UDP bounds, hostname sanitization, workflow pinning/permissions)
|
||||
- [ ] Web UI changes avoid unsafe DOM execution/injection patterns
|
||||
- [ ] No secrets added; no sensitive logging introduced
|
||||
- [ ] Error handling remains fail-safe and non-leaky
|
||||
- [ ] Dependency/workflow changes are supply-chain safe
|
||||
- [ ] Feature-flag names are valid and not typoed
|
||||
|
||||
## AI Review Behavior
|
||||
|
||||
- Prefer concrete, file/line-specific findings over generic guidance.
|
||||
- Prioritize **CRITICAL** and **IMPORTANT** findings.
|
||||
- Skip irrelevant framework checks not used by WLED.
|
||||
- If control-flow trust is unclear, ask for clarification instead of guessing.
|
||||
@@ -1,36 +0,0 @@
|
||||
---
|
||||
applyTo: "wled00/data/**"
|
||||
---
|
||||
# Web UI Coding Conventions
|
||||
|
||||
## Formatting
|
||||
|
||||
- Indent **HTML and JavaScript** with **tabs**
|
||||
- Indent **CSS** with **tabs**
|
||||
|
||||
## JavaScript Style
|
||||
|
||||
- **camelCase** for functions and variables: `gId()`, `selectedFx`, `currentPreset`
|
||||
- Abbreviated helpers are common: `d` for `document`, `gId()` for `getElementById()`
|
||||
|
||||
## Key Files
|
||||
|
||||
- `index.htm` — main interface
|
||||
- `index.js` — functions that manage / update the main interface
|
||||
- `settings*.htm` — configuration pages
|
||||
- `*.css` — stylesheets (inlined during build)
|
||||
- `common.js` — helper functions
|
||||
|
||||
**Reuse shared helpers from `common.js` whenever possible** instead of duplicating utilities in page-local scripts.
|
||||
|
||||
## Accessibility & Interaction
|
||||
|
||||
The WLED web UI targets commonly used browser/platform combinations: desktop browsers on Mac and PC (primarily pointer-driven, touch rare),
|
||||
and touch-only devices (phones, tablets). If possible, keep the UI accessible to users with disabilities.
|
||||
Full keyboard operability is not a strict requirement - adding keyboard shortcuts should be a case-by-case decision.
|
||||
|
||||
## Build Integration
|
||||
|
||||
Files in this directory are processed by `tools/cdata.js` into generated headers
|
||||
(`wled00/html_*.h`, `wled00/js_*.h`).
|
||||
Run `npm run build` after any change. **Never edit generated headers directly.**
|
||||
@@ -29,7 +29,6 @@ License along with NeoPixel. If not, see
|
||||
#pragma once
|
||||
|
||||
#if defined(ARDUINO_ARCH_ESP32)
|
||||
#if !defined(WLED_USE_SHARED_RMT) // V5 fix: don't compile this file on unsupported platforms
|
||||
|
||||
// Use the NeoEspRmtSpeed types from the driver-based implementation
|
||||
#include <NeoPixelBus.h>
|
||||
@@ -468,4 +467,3 @@ typedef NeoEsp32RmtHI7Ws2805InvertedMethod NeoEsp32RmtHI7Ws2814InvertedMethod;
|
||||
#endif // !defined(CONFIG_IDF_TARGET_ESP32C3)
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -5,17 +5,15 @@
|
||||
*
|
||||
*/
|
||||
|
||||
#if defined(__XTENSA__) && defined(ESP32)
|
||||
|
||||
#include "esp_idf_version.h"
|
||||
#include "sdkconfig.h"
|
||||
|
||||
/* If the Bluetooth driver has hooked the high-priority interrupt, we piggyback on it and don't need this. */
|
||||
#if !defined(CONFIG_BTDM_CTRL_HLI) && ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0)
|
||||
#if defined(__XTENSA__) && defined(ESP32) && !defined(CONFIG_BTDM_CTRL_HLI)
|
||||
|
||||
#include <freertos/xtensa_context.h>
|
||||
#include "sdkconfig.h"
|
||||
#include "soc/soc.h"
|
||||
|
||||
/* If the Bluetooth driver has hooked the high-priority interrupt, we piggyback on it and don't need this. */
|
||||
#ifndef CONFIG_BTDM_CTRL_HLI
|
||||
|
||||
/*
|
||||
Select interrupt based on system check level
|
||||
- Base ESP32: could be 4 or 5, depends on platform config
|
||||
@@ -260,5 +258,6 @@ _highint_stack_switch:
|
||||
.global ld_include_hli_vectors_rmt
|
||||
ld_include_hli_vectors_rmt:
|
||||
|
||||
|
||||
#endif // CONFIG_BTDM_CTRL_HLI
|
||||
#endif // XTensa
|
||||
@@ -29,7 +29,7 @@ License along with NeoPixel. If not, see
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
#if defined(ARDUINO_ARCH_ESP32) && ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0)
|
||||
#if defined(ARDUINO_ARCH_ESP32)
|
||||
|
||||
#include <algorithm>
|
||||
#include "esp_idf_version.h"
|
||||
@@ -504,4 +504,4 @@ esp_err_t NeoEsp32RmtHiMethodDriver::WaitForTxDone(rmt_channel_t channel, TickTy
|
||||
return rv;
|
||||
}
|
||||
|
||||
#endif
|
||||
#endif
|
||||
Generated
+31
-33
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"name": "wled",
|
||||
"version": "17.0.0-devV5",
|
||||
"version": "0.16.0-alpha",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "wled",
|
||||
"version": "17.0.0-devV5",
|
||||
"version": "0.16.0-alpha",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"clean-css": "^5.3.3",
|
||||
"html-minifier-terser": "^7.2.0",
|
||||
"nodemon": "^3.1.14",
|
||||
"nodemon": "^3.1.9",
|
||||
"web-resource-inliner": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -111,13 +111,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/binary-extensions": {
|
||||
"version": "2.3.0",
|
||||
@@ -132,15 +129,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
|
||||
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/braces": {
|
||||
@@ -216,6 +211,12 @@
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||
@@ -523,18 +524,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "10.2.4",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
|
||||
"integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^5.0.2"
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
@@ -554,15 +552,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/nodemon": {
|
||||
"version": "3.1.14",
|
||||
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz",
|
||||
"integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==",
|
||||
"version": "3.1.9",
|
||||
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.9.tgz",
|
||||
"integrity": "sha512-hdr1oIb2p6ZSxu3PB2JWWYS7ZQ0qvaZsc3hK8DR8f02kRzc8rjYmxAIvdz+aYC+8F2IjNaB7HMcSDg8nQpJxyg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chokidar": "^3.5.2",
|
||||
"debug": "^4",
|
||||
"ignore-by-default": "^1.0.1",
|
||||
"minimatch": "^10.2.1",
|
||||
"minimatch": "^3.1.2",
|
||||
"pstree.remy": "^1.1.8",
|
||||
"semver": "^7.5.3",
|
||||
"simple-update-notifier": "^2.0.0",
|
||||
@@ -611,9 +609,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
|
||||
+6
-6
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wled",
|
||||
"version": "17.0.0-devV5",
|
||||
"version": "0.16.0-alpha",
|
||||
"description": "Tools for WLED project",
|
||||
"main": "tools/cdata.js",
|
||||
"directories": {
|
||||
@@ -14,21 +14,21 @@
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/wled/WLED.git"
|
||||
"url": "git+https://github.com/wled-dev/WLED.git"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"bugs": {
|
||||
"url": "https://github.com/wled/WLED/issues"
|
||||
"url": "https://github.com/wled-dev/WLED/issues"
|
||||
},
|
||||
"homepage": "https://github.com/wled/WLED#readme",
|
||||
"homepage": "https://github.com/wled-dev/WLED#readme",
|
||||
"dependencies": {
|
||||
"clean-css": "^5.3.3",
|
||||
"html-minifier-terser": "^7.2.0",
|
||||
"web-resource-inliner": "^7.0.0",
|
||||
"nodemon": "^3.1.14"
|
||||
"nodemon": "^3.1.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
# Add a section to the linker script to store our dynamic arrays
|
||||
# This is implemented as a pio post-script to ensure that we can
|
||||
# place our linker script at the correct point in the command arguments.
|
||||
Import("env")
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
# Linker script fragment injected into the rodata output section of whichever
|
||||
# platform we're building for. Placed just before the end-of-rodata marker so
|
||||
# that the dynarray entries land in flash rodata and are correctly sorted.
|
||||
DYNARRAY_INJECTION = (
|
||||
"\n /* dynarray: WLED dynamic module arrays */\n"
|
||||
" . = ALIGN(0x10);\n"
|
||||
" KEEP(*(SORT_BY_INIT_PRIORITY(.dynarray.*)))\n"
|
||||
" "
|
||||
)
|
||||
|
||||
|
||||
def inject_before_marker(path, marker):
|
||||
"""Patch a linker script file in-place, inserting DYNARRAY_INJECTION before marker."""
|
||||
original = path.read_text()
|
||||
marker_pos = original.find(marker)
|
||||
if marker_pos < 0:
|
||||
raise RuntimeError(
|
||||
f"DYNARRAY injection marker not found in linker script: path={path}, marker={marker!r}"
|
||||
)
|
||||
patched = original[:marker_pos] + DYNARRAY_INJECTION + original[marker_pos:]
|
||||
path.write_text(patched)
|
||||
|
||||
|
||||
if env.get("PIOPLATFORM") == "espressif32":
|
||||
# Find sections.ld on the linker search path (LIBPATH).
|
||||
sections_ld_path = None
|
||||
for ld_dir in env.get("LIBPATH", []):
|
||||
candidate = Path(str(ld_dir)) / "sections.ld"
|
||||
if candidate.exists():
|
||||
sections_ld_path = candidate
|
||||
break
|
||||
|
||||
if sections_ld_path is not None:
|
||||
# Inject inside the existing .flash.rodata output section, just before
|
||||
# _rodata_end. IDF v5 enforces zero gaps between adjacent output
|
||||
# sections via ASSERT statements, so INSERT AFTER .flash.rodata would
|
||||
# fail. Injecting inside the section creates no new output section and
|
||||
# leaves the ASSERTs satisfied.
|
||||
build_dir = Path(env.subst("$BUILD_DIR"))
|
||||
patched_path = build_dir / "dynarray_sections.ld"
|
||||
shutil.copy(sections_ld_path, patched_path)
|
||||
inject_before_marker(patched_path, "_rodata_end = ABSOLUTE(.);")
|
||||
|
||||
# Replace "sections.ld" in LINKFLAGS with an absolute path to our
|
||||
# patched copy. The flag may appear as a bare token, combined as
|
||||
# "-Tsections.ld", or split across two tokens ("-T", "sections.ld").
|
||||
patched_str = str(patched_path)
|
||||
new_flags = []
|
||||
skip_next = False
|
||||
for flag in env.get("LINKFLAGS", []):
|
||||
if skip_next:
|
||||
new_flags.append(patched_str if flag == "sections.ld" else flag)
|
||||
skip_next = False
|
||||
elif flag == "-T":
|
||||
new_flags.append(flag)
|
||||
skip_next = True
|
||||
else:
|
||||
new_flags.append(flag.replace("sections.ld", patched_str))
|
||||
env.Replace(LINKFLAGS=new_flags)
|
||||
else:
|
||||
# Assume sections.ld will be built (ESP-IDF format); add a post-action to patch it
|
||||
# TODO: consider using ESP-IDF linker fragment (https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-guides/linker-script-generation.html)
|
||||
# For now, patch after building
|
||||
sections_ld = Path(env.subst("$BUILD_DIR")) / "sections.ld"
|
||||
def patch_sections_ld(target, source, env):
|
||||
inject_before_marker(sections_ld, "_rodata_end = ABSOLUTE(.);")
|
||||
env.AddPostAction(str(sections_ld), patch_sections_ld)
|
||||
|
||||
elif env.get("PIOPLATFORM") == "espressif8266":
|
||||
# The ESP8266 framework preprocesses eagle.app.v6.common.ld.h into
|
||||
# local.eagle.app.v6.common.ld in $BUILD_DIR/ld/ at build time. Register
|
||||
# a post-action on that generated file so the injection happens after
|
||||
# C-preprocessing but before linking.
|
||||
build_ld = Path(env.subst("$BUILD_DIR")) / "ld" / "local.eagle.app.v6.common.ld"
|
||||
|
||||
def patch_esp8266_ld(target, source, env):
|
||||
inject_before_marker(build_ld, "_irom0_text_end = ABSOLUTE(.);")
|
||||
|
||||
env.AddPostAction(str(build_ld), patch_esp8266_ld)
|
||||
@@ -1,12 +0,0 @@
|
||||
# FastLED exports some weak cxx symbols as a way of managing integration with C-only
|
||||
# projects that cause it to be preferentially linked instead of unused code being
|
||||
# discarded like other libraries. This causes not only bloat of the final binaries
|
||||
# but can incorrectly invoke some of their driver framework on some platforms.
|
||||
#
|
||||
# Solve this problem by moving the cxx library up in the linker command line, so
|
||||
# that it will be chosen over FastLED's.
|
||||
Import("env")
|
||||
|
||||
if "-lcxx" in env["LIBS"]:
|
||||
env["LIBS"].remove("-lcxx")
|
||||
env["LIBS"].insert(0, "-lcxx")
|
||||
+18
-121
@@ -1,8 +1,6 @@
|
||||
Import('env')
|
||||
from collections import deque
|
||||
from pathlib import Path # For OS-agnostic path manipulation
|
||||
import re
|
||||
from urllib.parse import urlparse
|
||||
from click import secho
|
||||
from SCons.Script import Exit
|
||||
from platformio.builder.tools.piolib import LibBuilderBase
|
||||
@@ -27,117 +25,25 @@ def find_usermod(mod: str) -> Path:
|
||||
return mp
|
||||
raise RuntimeError(f"Couldn't locate module {mod} in usermods directory!")
|
||||
|
||||
# Names of external/registry deps listed in custom_usermods.
|
||||
# Populated during parsing below; read by is_wled_module() at configure time.
|
||||
_custom_usermod_names: set[str] = set()
|
||||
|
||||
# Matches any RFC-valid URL scheme (http, https, git, git+https, symlink, file, hg+ssh, etc.)
|
||||
_URL_SCHEME_RE = re.compile(r'^[a-zA-Z][a-zA-Z0-9+.-]*://')
|
||||
# SSH git URL: user@host:path (e.g. git@github.com:user/repo.git#tag)
|
||||
_SSH_URL_RE = re.compile(r'^[^@\s]+@[^@:\s]+:[^:\s]')
|
||||
# Explicit custom name: "LibName = <spec>" (PlatformIO [<name>=]<spec> form)
|
||||
_NAME_EQ_RE = re.compile(r'^([A-Za-z0-9_.-]+)\s*=\s*(\S.*)')
|
||||
|
||||
|
||||
def _is_external_entry(line: str) -> bool:
|
||||
"""Return True if line is a lib_deps-style external/registry entry."""
|
||||
if _NAME_EQ_RE.match(line): # "LibName = <spec>"
|
||||
return True
|
||||
if _URL_SCHEME_RE.match(line): # https://, git://, symlink://, etc.
|
||||
return True
|
||||
if _SSH_URL_RE.match(line): # git@github.com:user/repo.git
|
||||
return True
|
||||
if '@' in line: # "owner/Name @ ^1.0.0"
|
||||
return True
|
||||
if re.match(r'^[^/\s]+/[^/\s]+$', line): # "owner/Name"
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _predict_dep_name(entry: str) -> str | None:
|
||||
"""Predict the library name PlatformIO will assign to this dep (best-effort).
|
||||
|
||||
Accuracy relies on the library's manifest "name" matching the repo/package
|
||||
name in the spec. This holds for well-authored libraries; the libArchive
|
||||
check (which requires library.json) provides an early-failure safety net.
|
||||
"""
|
||||
entry = entry.strip()
|
||||
# "LibName = <spec>" — name is given explicitly; always use it
|
||||
m = _NAME_EQ_RE.match(entry)
|
||||
if m:
|
||||
return m.group(1).strip()
|
||||
# URL scheme: extract name from path
|
||||
if _URL_SCHEME_RE.match(entry):
|
||||
parsed = urlparse(entry)
|
||||
if parsed.netloc in ('github.com', 'gitlab.com', 'bitbucket.com'):
|
||||
parts = [p for p in parsed.path.split('/') if p]
|
||||
if len(parts) >= 2:
|
||||
name = parts[1]
|
||||
else:
|
||||
name = Path(parsed.path.rstrip('/')).name.strip()
|
||||
if name.endswith('.git'):
|
||||
name = name[:-4]
|
||||
return name or None
|
||||
# SSH git URL: git@github.com:user/repo.git#tag → repo
|
||||
if _SSH_URL_RE.match(entry):
|
||||
path_part = entry.split(':', 1)[1].split('#')[0].rstrip('/')
|
||||
name = Path(path_part).name
|
||||
return (name[:-4] if name.endswith('.git') else name) or None
|
||||
# Versioned registry: "owner/Name @ version" → Name
|
||||
if '@' in entry:
|
||||
name_part = entry.split('@')[0].strip()
|
||||
return name_part.split('/')[-1].strip() if '/' in name_part else name_part
|
||||
# Plain registry: "owner/Name" → Name
|
||||
if re.match(r'^[^/\s]+/[^/\s]+$', entry):
|
||||
return entry.split('/')[-1].strip()
|
||||
return None
|
||||
|
||||
|
||||
def is_wled_module(dep: LibBuilderBase) -> bool:
|
||||
"""Returns true if the specified library is a wled module."""
|
||||
return (
|
||||
usermod_dir in Path(dep.src_dir).parents
|
||||
or str(dep.name).startswith("wled-")
|
||||
or dep.name in _custom_usermod_names
|
||||
)
|
||||
"""Returns true if the specified library is a wled module
|
||||
"""
|
||||
return usermod_dir in Path(dep.src_dir).parents or str(dep.name).startswith("wled-")
|
||||
|
||||
## Script starts here
|
||||
# Process usermod option
|
||||
usermods = env.GetProjectOption("custom_usermods","")
|
||||
|
||||
## Script starts here — parse custom_usermods
|
||||
raw_usermods = env.GetProjectOption("custom_usermods", "")
|
||||
usermods_libdeps: list[str] = []
|
||||
# Handle "all usermods" case
|
||||
if usermods == '*':
|
||||
usermods = [f.name for f in usermod_dir.iterdir() if f.is_dir() and f.joinpath('library.json').exists()]
|
||||
else:
|
||||
usermods = usermods.split()
|
||||
|
||||
for line in raw_usermods.splitlines():
|
||||
line = line.strip()
|
||||
if not line or line.startswith('#') or line.startswith(';'):
|
||||
continue
|
||||
|
||||
if _is_external_entry(line):
|
||||
# External URL or registry entry: pass through to lib_deps unchanged.
|
||||
predicted = _predict_dep_name(line)
|
||||
if predicted:
|
||||
_custom_usermod_names.add(predicted)
|
||||
else:
|
||||
secho(
|
||||
f"WARNING: Cannot determine library name for custom_usermods entry "
|
||||
f"{line!r}. If it is not recognised as a WLED module at build time, "
|
||||
f"ensure its library.json 'name' matches the repo name.",
|
||||
fg="yellow", err=True)
|
||||
usermods_libdeps.append(line)
|
||||
else:
|
||||
# Bare name(s): split on whitespace for backwards compatibility.
|
||||
for token in line.split():
|
||||
if token == '*':
|
||||
for mod_path in sorted(usermod_dir.iterdir()):
|
||||
if mod_path.is_dir() and (mod_path / 'library.json').exists():
|
||||
_custom_usermod_names.add(mod_path.name)
|
||||
usermods_libdeps.append(f"symlink://{mod_path.resolve()}")
|
||||
else:
|
||||
resolved = find_usermod(token)
|
||||
_custom_usermod_names.add(resolved.name)
|
||||
usermods_libdeps.append(f"symlink://{resolved.resolve()}")
|
||||
|
||||
if usermods_libdeps:
|
||||
env.GetProjectConfig().set("env:" + env['PIOENV'], 'lib_deps', env.GetProjectOption('lib_deps') + usermods_libdeps)
|
||||
if usermods:
|
||||
# Inject usermods in to project lib_deps
|
||||
symlinks = [f"symlink://{find_usermod(mod).resolve()}" for mod in usermods]
|
||||
env.GetProjectConfig().set("env:" + env['PIOENV'], 'lib_deps', env.GetProjectOption('lib_deps') + symlinks)
|
||||
|
||||
# Utility function for assembling usermod include paths
|
||||
def cached_add_includes(dep, dep_cache: set, includes: deque):
|
||||
@@ -180,14 +86,6 @@ def wrapped_ConfigureProjectLibBuilder(xenv):
|
||||
# Add WLED's own dependencies
|
||||
for dir in extra_include_dirs:
|
||||
dep.env.PrependUnique(CPPPATH=str(dir))
|
||||
# Ensure debug info is emitted for this module's source files.
|
||||
# validate_modules.py uses `nm --defined-only -l` on the final ELF to check
|
||||
# that each module has at least one symbol placed in the binary. The -l flag
|
||||
# reads DWARF debug sections to map placed symbols back to their original source
|
||||
# files; without -g those sections are absent and the check cannot attribute any
|
||||
# symbol to a specific module. We scope this to usermods only — the main WLED
|
||||
# build and other libraries are unaffected.
|
||||
dep.env.AppendUnique(CCFLAGS=["-g"])
|
||||
# Enforce that libArchive is not set; we must link them directly to the executable
|
||||
if dep.lib_archive:
|
||||
broken_usermods.append(dep)
|
||||
@@ -195,10 +93,9 @@ def wrapped_ConfigureProjectLibBuilder(xenv):
|
||||
if broken_usermods:
|
||||
broken_usermods = [usermod.name for usermod in broken_usermods]
|
||||
secho(
|
||||
f"ERROR: libArchive=false is missing on usermod(s) {' '.join(broken_usermods)} -- "
|
||||
f"modules will not compile in correctly. Add '\"build\": {{\"libArchive\": false}}' "
|
||||
f"to their library.json.",
|
||||
fg="red", err=True)
|
||||
f"ERROR: libArchive=false is missing on usermod(s) {' '.join(broken_usermods)} -- modules will not compile in correctly",
|
||||
fg="red",
|
||||
err=True)
|
||||
Exit(1)
|
||||
|
||||
# Save the depbuilders list for later validation
|
||||
|
||||
@@ -2,7 +2,6 @@ Import('env')
|
||||
import os
|
||||
import shutil
|
||||
import gzip
|
||||
import json
|
||||
|
||||
OUTPUT_DIR = "build_output{}".format(os.path.sep)
|
||||
#OUTPUT_DIR = os.path.join("build_output")
|
||||
@@ -23,8 +22,7 @@ def create_release(source):
|
||||
release_name_def = _get_cpp_define_value(env, "WLED_RELEASE_NAME")
|
||||
if release_name_def:
|
||||
release_name = release_name_def.replace("\\\"", "")
|
||||
with open("package.json", "r") as package:
|
||||
version = json.load(package)["version"]
|
||||
version = _get_cpp_define_value(env, "WLED_VERSION")
|
||||
release_file = os.path.join(OUTPUT_DIR, "release", f"WLED_{version}_{release_name}.bin")
|
||||
release_gz_file = release_file + ".gz"
|
||||
print(f"Copying {source} to {release_file}")
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
Import('env')
|
||||
import subprocess
|
||||
import json
|
||||
import re
|
||||
|
||||
def get_github_repo():
|
||||
"""Extract GitHub repository name from git remote URL.
|
||||
|
||||
Uses the remote that the current branch tracks, falling back to 'origin'.
|
||||
This handles cases where repositories have multiple remotes or where the
|
||||
main remote is not named 'origin'.
|
||||
|
||||
Returns:
|
||||
str: Repository name in 'owner/repo' format for GitHub repos,
|
||||
'unknown' for non-GitHub repos, missing git CLI, or any errors.
|
||||
"""
|
||||
try:
|
||||
remote_name = 'origin' # Default fallback
|
||||
|
||||
# Try to get the remote for the current branch
|
||||
try:
|
||||
# Get current branch name
|
||||
branch_result = subprocess.run(['git', 'rev-parse', '--abbrev-ref', 'HEAD'],
|
||||
capture_output=True, text=True, check=True)
|
||||
current_branch = branch_result.stdout.strip()
|
||||
|
||||
# Get the remote for the current branch
|
||||
remote_result = subprocess.run(['git', 'config', f'branch.{current_branch}.remote'],
|
||||
capture_output=True, text=True, check=True)
|
||||
tracked_remote = remote_result.stdout.strip()
|
||||
|
||||
# Use the tracked remote if we found one
|
||||
if tracked_remote:
|
||||
remote_name = tracked_remote
|
||||
except subprocess.CalledProcessError:
|
||||
# If branch config lookup fails, continue with 'origin' as fallback
|
||||
pass
|
||||
|
||||
# Get the remote URL for the determined remote
|
||||
result = subprocess.run(['git', 'remote', 'get-url', remote_name],
|
||||
capture_output=True, text=True, check=True)
|
||||
remote_url = result.stdout.strip()
|
||||
|
||||
# Check if it's a GitHub URL
|
||||
if 'github.com' not in remote_url.lower():
|
||||
return None
|
||||
|
||||
# Parse GitHub URL patterns:
|
||||
# https://github.com/owner/repo.git
|
||||
# git@github.com:owner/repo.git
|
||||
# https://github.com/owner/repo
|
||||
|
||||
# Remove .git suffix if present
|
||||
if remote_url.endswith('.git'):
|
||||
remote_url = remote_url[:-4]
|
||||
|
||||
# Handle HTTPS URLs
|
||||
https_match = re.search(r'github\.com/([^/]+/[^/]+)', remote_url, re.IGNORECASE)
|
||||
if https_match:
|
||||
return https_match.group(1)
|
||||
|
||||
# Handle SSH URLs
|
||||
ssh_match = re.search(r'github\.com:([^/]+/[^/]+)', remote_url, re.IGNORECASE)
|
||||
if ssh_match:
|
||||
return ssh_match.group(1)
|
||||
|
||||
return None
|
||||
|
||||
except FileNotFoundError:
|
||||
# Git CLI is not installed or not in PATH
|
||||
return None
|
||||
except subprocess.CalledProcessError:
|
||||
# Git command failed (e.g., not a git repo, no remote, etc.)
|
||||
return None
|
||||
except Exception:
|
||||
# Any other unexpected error
|
||||
return None
|
||||
|
||||
# WLED version is managed by package.json; this is picked up in several places
|
||||
# - It's integrated in to the UI code
|
||||
# - Here, for wled_metadata.cpp
|
||||
# - The output_bins script
|
||||
# We always take it from package.json to ensure consistency
|
||||
with open("package.json", "r") as package:
|
||||
WLED_VERSION = json.load(package)["version"]
|
||||
|
||||
def has_def(cppdefs, name):
|
||||
""" Returns true if a given name is set in a CPPDEFINES collection """
|
||||
for f in cppdefs:
|
||||
if isinstance(f, tuple):
|
||||
f = f[0]
|
||||
if f == name:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def add_wled_metadata_flags(env, node):
|
||||
cdefs = env["CPPDEFINES"].copy()
|
||||
|
||||
if not has_def(cdefs, "WLED_REPO"):
|
||||
repo = get_github_repo()
|
||||
if repo:
|
||||
cdefs.append(("WLED_REPO", f"\\\"{repo}\\\""))
|
||||
|
||||
cdefs.append(("WLED_VERSION", WLED_VERSION))
|
||||
|
||||
# This transforms the node in to a Builder; it cannot be modified again
|
||||
return env.Object(
|
||||
node,
|
||||
CPPDEFINES=cdefs
|
||||
)
|
||||
|
||||
env.AddBuildMiddleware(
|
||||
add_wled_metadata_flags,
|
||||
"*/wled_metadata.cpp"
|
||||
)
|
||||
@@ -0,0 +1,8 @@
|
||||
Import('env')
|
||||
import json
|
||||
|
||||
PACKAGE_FILE = "package.json"
|
||||
|
||||
with open(PACKAGE_FILE, "r") as package:
|
||||
version = json.load(package)["version"]
|
||||
env.Append(BUILD_FLAGS=[f"-DWLED_VERSION={version}"])
|
||||
+32
-129
@@ -1,11 +1,16 @@
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from pathlib import Path # For OS-agnostic path manipulation
|
||||
from typing import Iterable
|
||||
from click import secho
|
||||
from SCons.Script import Action, Exit
|
||||
Import("env")
|
||||
from platformio.builder.tools.piolib import LibBuilderBase
|
||||
|
||||
_ATTR = re.compile(r'\bDW_AT_(name|comp_dir)\b')
|
||||
|
||||
def is_wled_module(env, dep: LibBuilderBase) -> bool:
|
||||
"""Returns true if the specified library is a wled module
|
||||
"""
|
||||
usermod_dir = Path(env["PROJECT_DIR"]).resolve() / "usermods"
|
||||
return usermod_dir in Path(dep.src_dir).parents or str(dep.name).startswith("wled-")
|
||||
|
||||
|
||||
def read_lines(p: Path):
|
||||
@@ -14,164 +19,62 @@ def read_lines(p: Path):
|
||||
return f.readlines()
|
||||
|
||||
|
||||
def _get_readelf_path(env) -> str:
|
||||
""" Derive the readelf tool path from the build environment """
|
||||
# Derive from the C compiler: xtensa-esp32-elf-gcc → xtensa-esp32-elf-readelf
|
||||
cc = Path(env.subst("$CC"))
|
||||
return str(cc.with_name(re.sub(r'(gcc|g\+\+)$', 'readelf', cc.name)))
|
||||
def check_map_file_objects(map_file: list[str], dirs: Iterable[str]) -> set[str]:
|
||||
""" Identify which dirs contributed to the final build
|
||||
|
||||
|
||||
def check_elf_modules(elf_path: Path, env, module_lib_builders) -> set[str]:
|
||||
""" Check which modules have at least one compilation unit in the ELF.
|
||||
|
||||
The map file is not a reliable source for this: with LTO, original object
|
||||
file paths are replaced by temporary ltrans.o partitions in all output
|
||||
sections, making per-module attribution impossible from the map alone.
|
||||
Instead we invoke readelf --debug-dump=info --dwarf-depth=1 on the ELF,
|
||||
which reads only the top-level compilation-unit DIEs from .debug_info.
|
||||
Each CU corresponds to one source file; matching DW_AT_comp_dir +
|
||||
DW_AT_name against the module src_dirs is sufficient to confirm a module
|
||||
was compiled into the ELF. The output volume is proportional to the
|
||||
number of source files, not the number of symbols.
|
||||
|
||||
Returns the set of build_dir basenames for confirmed modules.
|
||||
Returns the (sub)set of dirs that are found in the output ELF
|
||||
"""
|
||||
readelf_path = _get_readelf_path(env)
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[readelf_path, "--debug-dump=info", "--dwarf-depth=1", str(elf_path)],
|
||||
capture_output=True, text=True, errors="ignore", timeout=120,
|
||||
)
|
||||
output = result.stdout
|
||||
if result.returncode != 0 or result.stderr.strip():
|
||||
secho(f"WARNING: readelf exited {result.returncode}: {result.stderr.strip()}", fg="yellow", err=True)
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError, OSError) as e:
|
||||
secho(f"WARNING: readelf failed ({e}); skipping per-module validation", fg="yellow", err=True)
|
||||
return {Path(b.build_dir).name for b in module_lib_builders} # conservative pass
|
||||
# Pattern to match symbols in object directories
|
||||
# Join directories into alternation
|
||||
usermod_dir_regex = "|".join([re.escape(dir) for dir in dirs])
|
||||
# Matches nonzero address, any size, and any path in a matching directory
|
||||
object_path_regex = re.compile(r"0x0*[1-9a-f][0-9a-f]*\s+0x[0-9a-f]+\s+\S+[/\\](" + usermod_dir_regex + r")[/\\]\S+\.o")
|
||||
|
||||
remaining = {Path(str(b.src_dir)): Path(b.build_dir).name for b in module_lib_builders}
|
||||
found = set()
|
||||
project_dir = Path(env.subst("$PROJECT_DIR"))
|
||||
|
||||
def _flush_cu(comp_dir: str | None, name: str | None) -> None:
|
||||
"""Match one completed CU against remaining builders."""
|
||||
if not name or not remaining:
|
||||
return
|
||||
p = Path(name)
|
||||
src_path = (Path(comp_dir) / p) if (comp_dir and not p.is_absolute()) else p
|
||||
# In arduino+espidf dual-framework builds the IDF toolchain sets DW_AT_comp_dir
|
||||
# to the virtual path "/IDF_PROJECT" rather than the real project root, so
|
||||
# src_path won't match. Pre-compute a fallback using $PROJECT_DIR and check
|
||||
# both candidates in a single pass.
|
||||
use_fallback = not p.is_absolute() and comp_dir and Path(comp_dir) != project_dir
|
||||
src_path_real = project_dir / p if use_fallback else None
|
||||
for src_dir in list(remaining):
|
||||
if src_path.is_relative_to(src_dir) or (src_path_real and src_path_real.is_relative_to(src_dir)):
|
||||
found.add(remaining.pop(src_dir))
|
||||
return
|
||||
|
||||
# readelf emits one DW_TAG_compile_unit DIE per source file. Attributes
|
||||
# of interest:
|
||||
# DW_AT_name — source file (absolute, or relative to comp_dir)
|
||||
# DW_AT_comp_dir — compile working directory
|
||||
# Both appear as either a direct string or an indirect string:
|
||||
# DW_AT_name : foo.cpp
|
||||
# DW_AT_name : (indirect string, offset: 0x…): foo.cpp
|
||||
# Taking the portion after the *last* ": " on the line handles both forms.
|
||||
|
||||
comp_dir = name = None
|
||||
for line in output.splitlines():
|
||||
if 'Compilation Unit @' in line:
|
||||
_flush_cu(comp_dir, name)
|
||||
comp_dir = name = None
|
||||
continue
|
||||
if not remaining:
|
||||
break # all builders matched
|
||||
m = _ATTR.search(line)
|
||||
if m:
|
||||
_, _, val = line.rpartition(': ')
|
||||
val = val.strip()
|
||||
if m.group(1) == 'name':
|
||||
name = val
|
||||
else:
|
||||
comp_dir = val
|
||||
_flush_cu(comp_dir, name) # flush the last CU
|
||||
|
||||
for line in map_file:
|
||||
matches = object_path_regex.findall(line)
|
||||
for m in matches:
|
||||
found.add(m)
|
||||
return found
|
||||
|
||||
|
||||
def count_usermod_objects(map_file: list[str]) -> int:
|
||||
""" Returns the number of usermod objects in the usermod list.
|
||||
|
||||
Computes the count from the address span between the .dynarray.usermods.0
|
||||
and .dynarray.usermods.99999 sentinel sections. This mirrors the
|
||||
DYNARRAY_LENGTH macro and is reliable under LTO, where all entries are
|
||||
merged into a single ltrans partition so counting section occurrences
|
||||
always yields 1 regardless of the true count.
|
||||
"""
|
||||
ENTRY_SIZE = 4 # sizeof(Usermod*) on 32-bit targets
|
||||
addr_begin = None
|
||||
addr_end = None
|
||||
|
||||
for i, line in enumerate(map_file):
|
||||
stripped = line.strip()
|
||||
if stripped == '.dynarray.usermods.0':
|
||||
if i + 1 < len(map_file):
|
||||
m = re.search(r'0x([0-9a-fA-F]+)', map_file[i + 1])
|
||||
if m:
|
||||
addr_begin = int(m.group(1), 16)
|
||||
elif stripped == '.dynarray.usermods.99999':
|
||||
if i + 1 < len(map_file):
|
||||
m = re.search(r'0x([0-9a-fA-F]+)', map_file[i + 1])
|
||||
if m:
|
||||
addr_end = int(m.group(1), 16)
|
||||
if addr_begin is not None and addr_end is not None:
|
||||
break
|
||||
|
||||
if addr_begin is None or addr_end is None:
|
||||
return 0
|
||||
return (addr_end - addr_begin) // ENTRY_SIZE
|
||||
""" Returns the number of usermod objects in the usermod list """
|
||||
# Count the number of entries in the usermods table section
|
||||
return len([x for x in map_file if ".dtors.tbl.usermods.1" in x])
|
||||
|
||||
|
||||
def validate_map_file(source, target, env):
|
||||
""" Validate that all modules appear in the output build """
|
||||
build_dir = Path(env.subst("$BUILD_DIR"))
|
||||
map_file_path = build_dir / env.subst("${PROGNAME}.map")
|
||||
map_file_path = build_dir / env.subst("${PROGNAME}.map")
|
||||
|
||||
if not map_file_path.exists():
|
||||
secho(f"ERROR: Map file not found: {map_file_path}", fg="red", err=True)
|
||||
Exit(1)
|
||||
|
||||
# Identify the WLED module builders, set by load_usermods.py
|
||||
module_lib_builders = env.get('WLED_MODULES')
|
||||
if module_lib_builders is None:
|
||||
secho("ERROR: WLED_MODULES not set — ensure load_usermods.py is run as a pre: script", fg="red", err=True)
|
||||
Exit(1)
|
||||
module_lib_builders = env['WLED_MODULES']
|
||||
|
||||
# Extract the values we care about
|
||||
modules = {Path(builder.build_dir).name: builder.name for builder in module_lib_builders}
|
||||
secho(f"INFO: {len(modules)} libraries included as WLED optional/user modules")
|
||||
secho(f"INFO: {len(modules)} libraries linked as WLED optional/user modules")
|
||||
|
||||
# Now parse the map file
|
||||
map_file_contents = read_lines(map_file_path)
|
||||
usermod_object_count = count_usermod_objects(map_file_contents)
|
||||
secho(f"INFO: {usermod_object_count} usermod object entries found")
|
||||
|
||||
elf_path = build_dir / env.subst("${PROGNAME}.elf")
|
||||
|
||||
confirmed_modules = check_elf_modules(elf_path, env, module_lib_builders)
|
||||
if confirmed_modules:
|
||||
secho(f"INFO: Code from usermod libraries found in binary: {', '.join(confirmed_modules)}")
|
||||
# else - if there's no usermods found, don't generate a message. If we're legitimately missing all entries, the error report on the
|
||||
# next line will trip; and if the usermod set is expected to be empty, then there's no need for yet another null message.
|
||||
secho(f"INFO: {usermod_object_count} usermod object entries")
|
||||
|
||||
confirmed_modules = check_map_file_objects(map_file_contents, modules.keys())
|
||||
missing_modules = [modname for mdir, modname in modules.items() if mdir not in confirmed_modules]
|
||||
if missing_modules:
|
||||
secho(
|
||||
f"ERROR: No symbols from {missing_modules} found in linked output!",
|
||||
f"ERROR: No object files from {missing_modules} found in linked output!",
|
||||
fg="red",
|
||||
err=True)
|
||||
Exit(1)
|
||||
return None
|
||||
|
||||
Import("env")
|
||||
env.Append(LINKFLAGS=[env.subst("-Wl,--Map=${BUILD_DIR}/${PROGNAME}.map")])
|
||||
env.AddPostAction("$BUILD_DIR/${PROGNAME}.elf", Action(validate_map_file, cmdstr='Checking linked optional modules (usermods) in map file'))
|
||||
|
||||
+163
-468
@@ -10,43 +10,49 @@
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# CI/release binaries
|
||||
default_envs =
|
||||
; nodemcuv2
|
||||
; esp8266_2m
|
||||
; esp01_1m_full
|
||||
nodemcuv2_160 ;; 8266 regression test build
|
||||
; esp8266_2m_160
|
||||
; esp01_1m_full_160
|
||||
; nodemcuv2_compat
|
||||
; esp8266_2m_compat
|
||||
; esp01_1m_full_compat
|
||||
esp32dev_V4 ;; V4 regression test build
|
||||
esp32dev
|
||||
esp32dev_debug
|
||||
esp32_eth
|
||||
esp32_wrover
|
||||
lolin_s2_mini ;; TODO: disabled NeoEsp32RmtMethodIsr
|
||||
esp32c3dev
|
||||
; esp32s3dev_16MB_opi ;; TODO: disabled NeoEsp32RmtMethodIsr
|
||||
; esp32s3dev_8MB_opi ;; TODO: disabled NeoEsp32RmtMethodIsr
|
||||
esp32s3_4M_qspi ;; TODO: disabled NeoEsp32RmtMethodIsr
|
||||
esp32c3dev_qio
|
||||
; esp32S3_wroom2 ;; TODO: disabled NeoEsp32RmtMethodIsr
|
||||
; esp32s3dev_16MB_opi ;; TODO: disabled NeoEsp32RmtMethodIsr
|
||||
esp32s3dev_8MB_opi ;; TODO: disabled NeoEsp32RmtMethodIsr
|
||||
; esp32s3dev_8MB_qspi ;; TODO: disabled NeoEsp32RmtMethodIsr
|
||||
; esp32s3dev_8MB_none ;; TODO: disabled NeoEsp32RmtMethodIsr
|
||||
; esp32s3_4M_qspi ;; TODO: disabled NeoEsp32RmtMethodIsr
|
||||
; usermods ;; TODO: disabled until the core is building
|
||||
default_envs = nodemcuv2, esp8266_2m, esp01_1m_full, nodemcuv2_160, esp8266_2m_160, esp01_1m_full_160, nodemcuv2_compat, esp8266_2m_compat, esp01_1m_full_compat, esp32dev, esp32_eth, lolin_s2_mini, esp32c3dev, esp32s3dev_16MB_opi, esp32s3dev_8MB_opi, esp32s3_4M_qspi, esp32_wrover, usermods
|
||||
|
||||
src_dir = ./wled00
|
||||
data_dir = ./wled00/data
|
||||
build_cache_dir = ~/.buildcache
|
||||
extra_configs =
|
||||
platformio_override.ini
|
||||
platformio_release.ini
|
||||
|
||||
[common]
|
||||
# ------------------------------------------------------------------------------
|
||||
# PLATFORM:
|
||||
# !! DO NOT confuse platformio's ESP8266 development platform with Arduino core for ESP8266
|
||||
#
|
||||
# arduino core 2.6.3 = platformIO 2.3.2
|
||||
# arduino core 2.7.0 = platformIO 2.5.0
|
||||
# ------------------------------------------------------------------------------
|
||||
arduino_core_2_6_3 = espressif8266@2.3.3
|
||||
arduino_core_2_7_4 = espressif8266@2.6.2
|
||||
arduino_core_3_0_0 = espressif8266@3.0.0
|
||||
arduino_core_3_0_2 = espressif8266@3.2.0
|
||||
arduino_core_3_1_0 = espressif8266@4.1.0
|
||||
arduino_core_3_1_2 = espressif8266@4.2.1
|
||||
|
||||
# Development platforms
|
||||
arduino_core_develop = https://github.com/platformio/platform-espressif8266#develop
|
||||
arduino_core_git = https://github.com/platformio/platform-espressif8266#feature/stage
|
||||
|
||||
# Platform to use for ESP8266
|
||||
platform_wled_default = ${common.arduino_core_3_1_2}
|
||||
# We use 2.7.4.7 for all, includes PWM flicker fix and Wstring optimization
|
||||
#platform_packages = tasmota/framework-arduinoespressif8266 @ 3.20704.7
|
||||
platform_packages = platformio/toolchain-xtensa @ ~2.100300.220621 #2.40802.200502
|
||||
platformio/tool-esptool #@ ~1.413.0
|
||||
platformio/tool-esptoolpy #@ ~1.30000.0
|
||||
|
||||
## previous platform for 8266, in case of problems with the new one
|
||||
## you'll need makuna/NeoPixelBus@ 2.6.9 for arduino_core_3_0_2, which does not support Ucs890x
|
||||
;; platform_wled_default = ${common.arduino_core_3_0_2}
|
||||
;; platform_packages = tasmota/framework-arduinoespressif8266 @ 3.20704.7
|
||||
;; platformio/toolchain-xtensa @ ~2.40802.200502
|
||||
;; platformio/tool-esptool @ ~1.413.0
|
||||
;; platformio/tool-esptoolpy @ ~1.30000.0
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# FLAGS: DEBUG
|
||||
# esp8266 : see https://docs.platformio.org/en/latest/platforms/espressif8266.html#debug-level
|
||||
@@ -94,7 +100,6 @@ build_flags =
|
||||
-D DECODE_SAMSUNG=true
|
||||
-D DECODE_LG=true
|
||||
-DWLED_USE_MY_CONFIG
|
||||
-D WLED_PS_DONT_REPLACE_FX ; PS replacement FX are purely a flash memory saving feature, do not replace classic FX until we run out of flash
|
||||
|
||||
build_unflags =
|
||||
|
||||
@@ -103,19 +108,14 @@ ldscript_2m512k = eagle.flash.2m512.ld
|
||||
ldscript_2m1m = eagle.flash.2m1m.ld
|
||||
ldscript_4m1m = eagle.flash.4m1m.ld
|
||||
|
||||
default_usermods = ;; TODO: add back audioreactive once V5 compatible
|
||||
;; default_usermods = wizlights animartrix ;; for testing
|
||||
|
||||
[scripts_defaults]
|
||||
extra_scripts =
|
||||
pre:pio-scripts/set_metadata.py
|
||||
pre:pio-scripts/set_version.py
|
||||
post:pio-scripts/output_bins.py
|
||||
post:pio-scripts/strip-floats.py
|
||||
post:pio-scripts/dynarray.py
|
||||
pre:pio-scripts/user_config_copy.py
|
||||
pre:pio-scripts/load_usermods.py
|
||||
pre:pio-scripts/build_ui.py
|
||||
;;post:pio-scripts/fastled_cxx_workaround.py
|
||||
post:pio-scripts/validate_modules.py ;; double-check the build output usermods
|
||||
; post:pio-scripts/obj-dump.py ;; convenience script to create a disassembly dump of the firmware (hardcore debugging)
|
||||
|
||||
@@ -138,56 +138,39 @@ upload_speed = 115200
|
||||
# ------------------------------------------------------------------------------
|
||||
lib_compat_mode = strict
|
||||
lib_deps =
|
||||
crankyoldgit/IRremoteESP8266 @ 2.9.0
|
||||
;; neopixelbus dependancy moved into [8266] and [esp32_all_variants] sections
|
||||
ESPAsyncWebServerWLED = git+https://github.com/Aircoookie/ESPAsyncWebServer#ac44e32abf2a69ae650412fb6bc193c59ccac38a
|
||||
fastled/FastLED @ 3.6.0
|
||||
IRremoteESP8266 @ 2.8.2
|
||||
makuna/NeoPixelBus @ 2.8.3
|
||||
https://github.com/Aircoookie/ESPAsyncWebServer.git#v2.4.2
|
||||
marvinroger/AsyncMqttClient @ 0.9.0
|
||||
# for I2C interface
|
||||
;Wire
|
||||
# ESP-NOW library
|
||||
;gmag11/QuickESPNow @ ~0.7.0
|
||||
https://github.com/blazoncek/QuickESPNow.git#optional-debug
|
||||
#For use of the TTGO T-Display ESP32 Module with integrated TFT display uncomment the following line
|
||||
#TFT_eSPI
|
||||
#For compatible OLED display uncomment following
|
||||
#olikraus/U8g2 #@ ~2.33.15
|
||||
#For Dallas sensor uncomment following
|
||||
#paulstoffregen/OneWire @ ~2.3.8
|
||||
#For BME280 sensor uncomment following
|
||||
#BME280 @ ~3.0.0
|
||||
;adafruit/Adafruit BMP280 Library @ 2.1.0
|
||||
;adafruit/Adafruit CCS811 Library @ 1.0.4
|
||||
;adafruit/Adafruit Si7021 Library @ 1.4.0
|
||||
#For MAX1704x Lipo Monitor / Fuel Gauge uncomment following
|
||||
; https://github.com/adafruit/Adafruit_BusIO @ 1.14.5
|
||||
; https://github.com/adafruit/Adafruit_MAX1704X @ 1.0.2
|
||||
#For MPU6050 IMU uncomment follwoing
|
||||
;electroniccats/MPU6050 @1.0.1
|
||||
# SHT85
|
||||
;robtillaart/SHT85@~0.3.3
|
||||
|
||||
extra_scripts = ${scripts_defaults.extra_scripts}
|
||||
|
||||
[esp8266]
|
||||
# ------------------------------------------------------------------------------
|
||||
# PLATFORM:
|
||||
# !! DO NOT confuse platformio's ESP8266 development platform with Arduino core for ESP8266
|
||||
#
|
||||
# arduino core 2.6.3 = platformIO 2.3.2
|
||||
# arduino core 2.7.0 = platformIO 2.5.0
|
||||
# ------------------------------------------------------------------------------
|
||||
arduino_core_2_6_3 = espressif8266@2.3.3
|
||||
arduino_core_2_7_4 = espressif8266@2.6.2
|
||||
arduino_core_3_0_0 = espressif8266@3.0.0
|
||||
arduino_core_3_0_2 = espressif8266@3.2.0
|
||||
arduino_core_3_1_0 = espressif8266@4.1.0
|
||||
arduino_core_3_1_2 = espressif8266@4.2.1
|
||||
|
||||
# Development platforms
|
||||
arduino_core_develop = https://github.com/platformio/platform-espressif8266#develop
|
||||
arduino_core_git = https://github.com/platformio/platform-espressif8266#feature/stage
|
||||
|
||||
# Platform to use for ESP8266
|
||||
platform_wled_default = ${esp8266.arduino_core_3_1_2}
|
||||
# We use 2.7.4.7 for all, includes PWM flicker fix and Wstring optimization
|
||||
#platform_packages = tasmota/framework-arduinoespressif8266 @ 3.20704.7
|
||||
platform_packages = platformio/toolchain-xtensa @ ~2.100300.220621 #2.40802.200502
|
||||
platformio/tool-esptool #@ ~1.413.0
|
||||
platformio/tool-esptoolpy #@ ~1.30000.0
|
||||
|
||||
## previous platform for 8266, in case of problems with the new one
|
||||
## you'll need makuna/NeoPixelBus@ 2.6.9 for arduino_core_3_0_2, which does not support Ucs890x
|
||||
;; platform_wled_default = ${esp8266.arduino_core_3_0_2}
|
||||
;; platform_packages = tasmota/framework-arduinoespressif8266 @ 3.20704.7
|
||||
;; platformio/toolchain-xtensa @ ~2.40802.200502
|
||||
;; platformio/tool-esptool @ ~1.413.0
|
||||
;; platformio/tool-esptoolpy @ ~1.30000.0
|
||||
|
||||
platform = ${esp8266.platform_wled_default}
|
||||
build_unflags = ${common.build_unflags}
|
||||
custom_usermods =
|
||||
build_flags =
|
||||
-DESP8266
|
||||
-DFP_IN_IROM
|
||||
@@ -210,16 +193,13 @@ build_flags =
|
||||
-D PIO_FRAMEWORK_ARDUINO_MMU_CACHE16_IRAM48 ;; in case of linker errors like "section `.text1' will not fit in region `iram1_0_seg'"
|
||||
; -D PIO_FRAMEWORK_ARDUINO_MMU_CACHE16_IRAM48_SECHEAP_SHARED ;; (experimental) adds some extra heap, but may cause slowdown
|
||||
-D NON32XFER_HANDLER ;; ask forgiveness for PROGMEM misuse
|
||||
|
||||
lib_deps =
|
||||
#https://github.com/lorol/LITTLEFS.git
|
||||
ESPAsyncTCP @ 1.2.2
|
||||
ESPAsyncUDP
|
||||
ESP8266PWM
|
||||
https://github.com/tignioj/ArduinoUZlib.git#20aff95cd80c141f80bdbf66895409a0046d2c2f
|
||||
${env.lib_deps}
|
||||
https://github.com/Makuna/NeoPixelBus.git#a0919d1c10696614625978dd6fb750a1317a14ce ;; standard NPB version used in main branch
|
||||
|
||||
monitor_filters = esp8266_exception_decoder
|
||||
|
||||
;; compatibilty flags - same as 0.14.0 which seems to work better on some 8266 boards. Not using PIO_FRAMEWORK_ARDUINO_MMU_CACHE16_IRAM48
|
||||
build_flags_compat =
|
||||
@@ -246,31 +226,28 @@ lib_deps_compat =
|
||||
ESPAsyncTCP @ 1.2.2
|
||||
ESPAsyncUDP
|
||||
ESP8266PWM
|
||||
fastled/FastLED @ 3.6.0
|
||||
IRremoteESP8266 @ 2.8.2
|
||||
makuna/NeoPixelBus @ 2.7.9
|
||||
https://github.com/blazoncek/QuickESPNow.git#optional-debug
|
||||
https://github.com/tignioj/ArduinoUZlib.git#20aff95cd80c141f80bdbf66895409a0046d2c2f
|
||||
https://github.com/Aircoookie/ESPAsyncWebServer.git#v2.4.0
|
||||
|
||||
[esp32_all_variants]
|
||||
lib_deps =
|
||||
esp32async/AsyncTCP @ 3.4.10
|
||||
esp32async/AsyncTCP @ 3.4.7
|
||||
bitbank2/AnimatedGIF@^1.4.7
|
||||
https://github.com/Aircoookie/GifDecoder.git#bc3af189b6b1e06946569f6b4287f0b79a860f8e
|
||||
NeoPixelBus = git+https://github.com/Makuna/NeoPixelBus#76afe832f74b0738a3fa1bba0caf389ade9e7693
|
||||
https://github.com/Aircoookie/GifDecoder#bc3af18
|
||||
build_flags =
|
||||
-D CONFIG_ASYNC_TCP_USE_WDT=0
|
||||
-D CONFIG_ASYNC_TCP_STACK_SIZE=8192
|
||||
-D WLED_ENABLE_GIF
|
||||
|
||||
[esp32]
|
||||
platform = ${esp32_idf_V5.platform}
|
||||
platform = ${esp32_idf_V4.platform}
|
||||
platform_packages =
|
||||
build_unflags = ${esp32_idf_V5.build_unflags}
|
||||
build_flags = ${esp32_idf_V5.build_flags}
|
||||
lib_deps = ${esp32_idf_V5.lib_deps}
|
||||
lib_ignore = ${esp32_idf_V5.lib_ignore}
|
||||
monitor_filters = esp32_exception_decoder
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = ${esp32_idf_V4.build_flags}
|
||||
lib_deps = ${esp32_idf_V4.lib_deps}
|
||||
|
||||
tiny_partitions = tools/WLED_ESP32_2MB_noOTA.csv
|
||||
default_partitions = tools/WLED_ESP32_4MB_1MB_FS.csv
|
||||
@@ -284,25 +261,15 @@ board_build.partitions = ${esp32.default_partitions} ;; default partioning for
|
||||
AR_build_flags = ;; -fsingle-precision-constant ;; forces ArduinoFFT to use float math (2x faster)
|
||||
AR_lib_deps = ;; for pre-usermod-library platformio_override compatibility
|
||||
|
||||
|
||||
[esp32_idf_V4]
|
||||
;; build environment for ESP32 using ESP-IDF 4.4.x / arduino-esp32 v2.0.5
|
||||
;; *** important: build flags from esp32_idf_V4 are inherited by _all_ esp32-based MCUs: esp32, esp32s2, esp32s3, esp32c3
|
||||
;;
|
||||
;; please note that you can NOT update existing ESP32 installs with a "V4" build. Also updating by OTA will not work properly.
|
||||
;; You need to completely erase your device (esptool erase_flash) first, then install the "V4" build from VSCode+platformio.
|
||||
|
||||
;; tasmota platform (default)
|
||||
platform = https://github.com/tasmota/platform-espressif32/releases/download/2024.06.00/platform-espressif32.zip ;; Tasmota Arduino Core 2.0.18 with IPv6 support, based on IDF 4.4.8
|
||||
platform_packages =
|
||||
|
||||
;; espressif platform (optional - needs 300KB extra flash)
|
||||
;;; arduino-esp32 2.0.17 + esp-idf 4.4.7
|
||||
;; platform = espressif32@ ~6.13.0
|
||||
;; platform_packages =
|
||||
;;; arduino-esp32 2.0.14 + esp-idf 4.4.6
|
||||
;; platform = espressif32@ ~6.6.0
|
||||
;; platform_packages = platformio/framework-arduinoespressif32 @ 3.20014.231204
|
||||
|
||||
;; select arduino-esp32 v2.0.9 (arduino-esp32 2.0.10 thru 2.0.14 are buggy so avoid them)
|
||||
platform = https://github.com/tasmota/platform-espressif32/releases/download/2023.06.02/platform-espressif32.zip ;; Tasmota Arduino Core 2.0.9 with IPv6 support, based on IDF 4.4.4
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = -g
|
||||
-Wshadow=compatible-local ;; emit warning in case a local variable "shadows" another local one
|
||||
@@ -311,47 +278,13 @@ build_flags = -g
|
||||
-D WLED_ENABLE_DMX_INPUT
|
||||
lib_deps =
|
||||
${esp32_all_variants.lib_deps}
|
||||
https://github.com/someweisguy/esp_dmx.git#47db25d8c515e76fabcf5fc5ab0b786f98eeade0
|
||||
https://github.com/someweisguy/esp_dmx.git#47db25d
|
||||
${env.lib_deps}
|
||||
lib_ignore =
|
||||
|
||||
[esp32_idf_V5]
|
||||
;; build environment for ESP32 using ESP-IDF 5.3.4 / arduino-esp32 v3.1.10
|
||||
platform = https://github.com/tasmota/platform-espressif32/releases/download/2026.02.30/platform-espressif32.zip
|
||||
platform_packages =
|
||||
build_unflags = ${common.build_unflags}
|
||||
-Wno-volatile ;; avoid warning on .c files: "-Wno-volatile only applies to c++ files"
|
||||
build_flags = -g
|
||||
;;-Wno-deprecated ;; disables a ton of warnings: implicit capture of 'this' via '[=]' is deprecated in C++20 [-Wdeprecated]
|
||||
-Wshadow=compatible-local ;; emit warning in case a local variable "shadows" another local one
|
||||
-DARDUINO_ARCH_ESP32
|
||||
; -DESP32=ESP32 ;; disabled to avoid compiler warning: "ESP32" redefined
|
||||
${esp32_all_variants.build_flags}
|
||||
-D WLED_USE_SHARED_RMT ;; ToDO: check if NeoESP32RmtHI is still needed with V5 (see discussion in PR#4838)
|
||||
-D WLED_DISABLE_INFRARED ;; TODO: remove once we have updated library for V5
|
||||
-D WLED_DISABLE_MQTT ;; TODO: remove once we have updated library for V5
|
||||
-D WLED_ENABLE_DMX_INPUT
|
||||
-D ESP32_ARDUINO_NO_RGB_BUILTIN ;; avoids RMT driver abort on startup "E (98) rmt(legacy): CONFLICT! driver_ng is not allowed to be used with the legacy driver"
|
||||
lib_deps =
|
||||
${esp32_all_variants.lib_deps}
|
||||
https://github.com/netmindz/esp_dmx/#esp-idf-v5-fixes
|
||||
${env.lib_deps}
|
||||
lib_ignore =
|
||||
NeoESP32RmtHI
|
||||
|
||||
[v5_pioarduino_workaround]
|
||||
# This is a disgusting workaround for a pioarduino "feature": if you don't ever mention lib_archive in your platformio.ini
|
||||
# it forces it off globally, causing unused code from all libraries to be included even if you don't use it.
|
||||
# This causes FastLED to activate its driver framework, stealing hardware resources we want to use.
|
||||
# To work around this, all that we have to do is mention lib_archive in any section, even if it's never used; then
|
||||
# pioarduino believes we know what we're doing, and lets us do what we want.
|
||||
lib_archive = yes
|
||||
|
||||
[esp32s2]
|
||||
;; generic definitions for all ESP32-S2 boards
|
||||
platform = ${esp32_idf_V5.platform}
|
||||
platform_packages = ${esp32_idf_V5.platform_packages}
|
||||
build_unflags = ${esp32_idf_V5.build_unflags}
|
||||
platform = ${esp32_idf_V4.platform}
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = -g
|
||||
-DARDUINO_ARCH_ESP32
|
||||
-DARDUINO_ARCH_ESP32S2
|
||||
@@ -361,18 +294,15 @@ build_flags = -g
|
||||
-DARDUINO_USB_MODE=0 ;; this flag is mandatory for ESP32-S2 !
|
||||
;; please make sure that the following flags are properly set (to 0 or 1) by your board.json, or included in your custom platformio_override.ini entry:
|
||||
;; ARDUINO_USB_CDC_ON_BOOT
|
||||
${esp32_idf_V5.build_flags}
|
||||
${esp32_idf_V4.build_flags}
|
||||
lib_deps =
|
||||
${esp32_idf_V5.lib_deps}
|
||||
lib_ignore = ${esp32_idf_V5.lib_ignore}
|
||||
${esp32_idf_V4.lib_deps}
|
||||
board_build.partitions = ${esp32.default_partitions} ;; default partioning for 4MB Flash - can be overridden in build envs
|
||||
monitor_filters = esp32_exception_decoder
|
||||
|
||||
[esp32c3]
|
||||
;; generic definitions for all ESP32-C3 boards
|
||||
platform = ${esp32_idf_V5.platform}
|
||||
platform_packages = ${esp32_idf_V5.platform_packages}
|
||||
build_unflags = ${esp32_idf_V5.build_unflags}
|
||||
platform = ${esp32_idf_V4.platform}
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = -g
|
||||
-DARDUINO_ARCH_ESP32
|
||||
-DARDUINO_ARCH_ESP32C3
|
||||
@@ -381,21 +311,18 @@ build_flags = -g
|
||||
-DARDUINO_USB_MODE=1 ;; this flag is mandatory for ESP32-C3
|
||||
;; please make sure that the following flags are properly set (to 0 or 1) by your board.json, or included in your custom platformio_override.ini entry:
|
||||
;; ARDUINO_USB_CDC_ON_BOOT
|
||||
${esp32_idf_V5.build_flags}
|
||||
${esp32_idf_V4.build_flags}
|
||||
lib_deps =
|
||||
${esp32_idf_V5.lib_deps}
|
||||
lib_ignore = ${esp32_idf_V5.lib_ignore}
|
||||
${esp32_idf_V4.lib_deps}
|
||||
board_build.partitions = ${esp32.default_partitions} ;; default partioning for 4MB Flash - can be overridden in build envs
|
||||
board_build.flash_mode = qio
|
||||
monitor_filters = esp32_exception_decoder
|
||||
|
||||
[esp32s3]
|
||||
;; generic definitions for all ESP32-S3 boards
|
||||
platform = ${esp32_idf_V5.platform}
|
||||
platform_packages = ${esp32_idf_V5.platform_packages}
|
||||
build_unflags = ${esp32_idf_V5.build_unflags}
|
||||
platform = ${esp32_idf_V4.platform}
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = -g
|
||||
;; -DESP32 ;; disabled to avoid compiler warning: "ESP32" redefined
|
||||
-DESP32
|
||||
-DARDUINO_ARCH_ESP32
|
||||
-DARDUINO_ARCH_ESP32S3
|
||||
-DCONFIG_IDF_TARGET_ESP32S3=1
|
||||
@@ -403,25 +330,26 @@ build_flags = -g
|
||||
-DCO
|
||||
;; please make sure that the following flags are properly set (to 0 or 1) by your board.json, or included in your custom platformio_override.ini entry:
|
||||
;; ARDUINO_USB_MODE, ARDUINO_USB_CDC_ON_BOOT
|
||||
${esp32_idf_V5.build_flags}
|
||||
${esp32_idf_V4.build_flags}
|
||||
lib_deps =
|
||||
${esp32_idf_V5.lib_deps}
|
||||
lib_ignore = ${esp32_idf_V5.lib_ignore}
|
||||
${esp32_idf_V4.lib_deps}
|
||||
board_build.partitions = ${esp32.large_partitions} ;; default partioning for 8MB flash - can be overridden in build envs
|
||||
upload_speed = 921600
|
||||
monitor_filters = esp32_exception_decoder
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# WLED BUILDS
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
[env:nodemcuv2]
|
||||
extends = esp8266
|
||||
board = nodemcuv2
|
||||
platform = ${common.platform_wled_default}
|
||||
platform_packages = ${common.platform_packages}
|
||||
board_build.ldscript = ${common.ldscript_4m1m}
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP8266\" #-DWLED_DISABLE_2D
|
||||
-D WLED_DISABLE_PARTICLESYSTEM2D
|
||||
custom_usermods = audioreactive
|
||||
lib_deps = ${esp8266.lib_deps}
|
||||
monitor_filters = esp8266_exception_decoder
|
||||
|
||||
[env:nodemcuv2_compat]
|
||||
extends = env:nodemcuv2
|
||||
@@ -431,23 +359,24 @@ platform_packages = ${esp8266.platform_packages_compat}
|
||||
build_flags = ${common.build_flags} ${esp8266.build_flags_compat} -D WLED_RELEASE_NAME=\"ESP8266_compat\" #-DWLED_DISABLE_2D
|
||||
-D WLED_DISABLE_PARTICLESYSTEM2D
|
||||
;; lib_deps = ${esp8266.lib_deps_compat} ;; experimental - use older NeoPixelBus 2.7.9
|
||||
custom_usermods = audioreactive
|
||||
|
||||
[env:nodemcuv2_160]
|
||||
extends = env:nodemcuv2
|
||||
board_build.f_cpu = 160000000L
|
||||
build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP8266_160\" #-DWLED_DISABLE_2D
|
||||
-D WLED_DISABLE_PARTICLESYSTEM2D
|
||||
custom_usermods = ${common.default_usermods}
|
||||
custom_usermods = audioreactive
|
||||
|
||||
[env:esp8266_2m]
|
||||
extends = esp8266
|
||||
board = esp_wroom_02
|
||||
platform = ${common.platform_wled_default}
|
||||
platform_packages = ${common.platform_packages}
|
||||
board_build.ldscript = ${common.ldscript_2m512k}
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP02\"
|
||||
-D WLED_DISABLE_PARTICLESYSTEM2D
|
||||
-D WLED_DISABLE_PARTICLESYSTEM1D
|
||||
custom_usermods = audioreactive
|
||||
lib_deps = ${esp8266.lib_deps}
|
||||
|
||||
[env:esp8266_2m_compat]
|
||||
extends = env:esp8266_2m
|
||||
@@ -457,7 +386,6 @@ platform_packages = ${esp8266.platform_packages_compat}
|
||||
build_flags = ${common.build_flags} ${esp8266.build_flags_compat} -D WLED_RELEASE_NAME=\"ESP02_compat\" #-DWLED_DISABLE_2D
|
||||
-D WLED_DISABLE_PARTICLESYSTEM1D
|
||||
-D WLED_DISABLE_PARTICLESYSTEM2D
|
||||
custom_usermods = audioreactive
|
||||
|
||||
[env:esp8266_2m_160]
|
||||
extends = env:esp8266_2m
|
||||
@@ -465,39 +393,19 @@ board_build.f_cpu = 160000000L
|
||||
build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP02_160\"
|
||||
-D WLED_DISABLE_PARTICLESYSTEM1D
|
||||
-D WLED_DISABLE_PARTICLESYSTEM2D
|
||||
custom_usermods = ${common.default_usermods}
|
||||
|
||||
[env:esp8266_2m_min]
|
||||
;; Minimal-feature build for ESP02 (2MB flash).
|
||||
;; Use this to recover from a failed OTA: flash via serial, then OTA-upload the regular esp8266_2m binary.
|
||||
;; OTA is intentionally kept enabled. All other optional features are stripped to minimise binary size.
|
||||
extends = esp8266
|
||||
board = esp_wroom_02
|
||||
board_build.ldscript = ${common.ldscript_2m512k}
|
||||
build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP02_min\"
|
||||
-D WLED_DISABLE_ALEXA
|
||||
-D WLED_DISABLE_HUESYNC
|
||||
-D WLED_DISABLE_INFRARED
|
||||
-D WLED_DISABLE_MQTT
|
||||
-D WLED_DISABLE_ADALIGHT
|
||||
-D WLED_DISABLE_LOXONE
|
||||
-D WLED_DISABLE_WEBSOCKETS
|
||||
-D WLED_DISABLE_ESPNOW
|
||||
-D WLED_DISABLE_2D
|
||||
-D WLED_DISABLE_PARTICLESYSTEM1D
|
||||
-D WLED_DISABLE_PARTICLESYSTEM2D
|
||||
-D WLED_DISABLE_PIXELFORGE
|
||||
-D WLED_DISABLE_IMPROV_WIFISCAN
|
||||
custom_usermods = audioreactive
|
||||
|
||||
[env:esp01_1m_full]
|
||||
extends = esp8266
|
||||
board = esp01_1m
|
||||
platform = ${common.platform_wled_default}
|
||||
platform_packages = ${common.platform_packages}
|
||||
board_build.ldscript = ${common.ldscript_1m128k}
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP01\" -D WLED_DISABLE_OTA
|
||||
; -D WLED_USE_REAL_MATH ;; may fix wrong sunset/sunrise times, at the cost of 7064 bytes FLASH and 975 bytes RAM
|
||||
-D WLED_DISABLE_PARTICLESYSTEM1D
|
||||
-D WLED_DISABLE_PARTICLESYSTEM2D
|
||||
-D WLED_DISABLE_PIXELFORGE
|
||||
lib_deps = ${esp8266.lib_deps}
|
||||
|
||||
[env:esp01_1m_full_compat]
|
||||
extends = env:esp01_1m_full
|
||||
@@ -507,7 +415,6 @@ platform_packages = ${esp8266.platform_packages_compat}
|
||||
build_flags = ${common.build_flags} ${esp8266.build_flags_compat} -D WLED_RELEASE_NAME=\"ESP01_compat\" -D WLED_DISABLE_OTA #-DWLED_DISABLE_2D
|
||||
-D WLED_DISABLE_PARTICLESYSTEM1D
|
||||
-D WLED_DISABLE_PARTICLESYSTEM2D
|
||||
-D WLED_DISABLE_PIXELFORGE
|
||||
|
||||
[env:esp01_1m_full_160]
|
||||
extends = env:esp01_1m_full
|
||||
@@ -516,286 +423,179 @@ build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=
|
||||
; -D WLED_USE_REAL_MATH ;; may fix wrong sunset/sunrise times, at the cost of 7064 bytes FLASH and 975 bytes RAM
|
||||
-D WLED_DISABLE_PARTICLESYSTEM1D
|
||||
-D WLED_DISABLE_PARTICLESYSTEM2D
|
||||
-D WLED_DISABLE_PIXELFORGE
|
||||
;; custom_usermods = audioreactive ;; pushed program flash size over the limits
|
||||
|
||||
[env:esp32dev_V4]
|
||||
;; uses V4 framework - for checking that the code still builds in V4
|
||||
board = esp32dev
|
||||
platform = ${esp32_idf_V4.platform}
|
||||
platform_packages = ${esp32_idf_V4.platform_packages}
|
||||
build_unflags = ${esp32_idf_V4.build_unflags}
|
||||
-D WLED_ENABLE_DMX_INPUT ;; TODO: fix lots of compile errors in dmx_input.cpp
|
||||
custom_usermods = ${common.default_usermods}
|
||||
build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=\"ESP32_V4\" #-D WLED_DISABLE_BROWNOUT_DET
|
||||
-DARDUINO_USB_CDC_ON_BOOT=0 ;; this flag is mandatory for "classic ESP32" when building with arduino-esp32 >=2.0.3
|
||||
-D WLED_USE_ETHERNET -D RLYPIN=-1 -D BTNPIN=-1 ;; TODO: this is just for testing - remove before merging to main
|
||||
lib_deps = ${esp32_idf_V4.lib_deps}
|
||||
lib_ignore = ${esp32_idf_V4.lib_ignore}
|
||||
esp_dmx ;; TODO: fix lots of compile errors in dmx_input.cpp
|
||||
monitor_filters = esp32_exception_decoder
|
||||
board_build.partitions = ${esp32.default_partitions}
|
||||
board_build.flash_mode = dio
|
||||
custom_usermods = audioreactive
|
||||
|
||||
[env:esp32dev]
|
||||
board = esp32dev
|
||||
platform = ${esp32_idf_V5.platform}
|
||||
platform_packages = ${esp32_idf_V5.platform_packages}
|
||||
build_unflags = ${esp32_idf_V5.build_unflags}
|
||||
custom_usermods = ${common.default_usermods}
|
||||
build_flags = ${common.build_flags} ${esp32_idf_V5.build_flags} -D WLED_RELEASE_NAME=\"ESP32\" #-D WLED_DISABLE_BROWNOUT_DET
|
||||
platform = ${esp32_idf_V4.platform}
|
||||
build_unflags = ${common.build_unflags}
|
||||
custom_usermods = audioreactive
|
||||
build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=\"ESP32_V4\" #-D WLED_DISABLE_BROWNOUT_DET
|
||||
-DARDUINO_USB_CDC_ON_BOOT=0 ;; this flag is mandatory for "classic ESP32" when building with arduino-esp32 >=2.0.3
|
||||
lib_deps = ${esp32_idf_V5.lib_deps}
|
||||
lib_ignore = ${esp32_idf_V5.lib_ignore}
|
||||
lib_deps = ${esp32_idf_V4.lib_deps}
|
||||
monitor_filters = esp32_exception_decoder
|
||||
board_build.partitions = ${esp32.default_partitions}
|
||||
board_build.flash_mode = dio
|
||||
|
||||
[env:esp32dev_debug]
|
||||
extends = env:esp32dev
|
||||
build_type = debug
|
||||
monitor_filters = esp32_exception_decoder
|
||||
upload_speed = 921600
|
||||
build_unflags = ${esp32_idf_V5.build_unflags}
|
||||
-D WLED_RELEASE_NAME=\"ESP32\"
|
||||
build_flags = ${common.build_flags} ${esp32_idf_V5.build_flags}
|
||||
-D WLED_DEBUG
|
||||
-D WLED_RELEASE_NAME=\"ESP32_DEBUG\"
|
||||
-DARDUINO_USB_CDC_ON_BOOT=0
|
||||
[env:esp32dev_8M]
|
||||
board = esp32dev
|
||||
platform = ${esp32_idf_V5.platform}
|
||||
platform_packages = ${esp32_idf_V5.platform_packages}
|
||||
custom_usermods = ${common.default_usermods}
|
||||
build_unflags = ${esp32_idf_V5.build_unflags}
|
||||
build_flags = ${common.build_flags} ${esp32_idf_V5.build_flags} -D WLED_RELEASE_NAME=\"ESP32_8M\" #-D WLED_DISABLE_BROWNOUT_DET
|
||||
-DARDUINO_USB_CDC_ON_BOOT=0 ;; this flag is mandatory for "classic ESP32" when building with arduino-esp32 >=2.0.3
|
||||
lib_deps = ${esp32_idf_V5.lib_deps}
|
||||
lib_ignore = ${esp32_idf_V5.lib_ignore}
|
||||
platform = ${esp32_idf_V4.platform}
|
||||
custom_usermods = audioreactive
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=\"ESP32_8M\" #-D WLED_DISABLE_BROWNOUT_DET
|
||||
lib_deps = ${esp32_idf_V4.lib_deps}
|
||||
monitor_filters = esp32_exception_decoder
|
||||
board_build.partitions = ${esp32.large_partitions}
|
||||
board_upload.flash_size = 8MB
|
||||
board_upload.maximum_size = 8388608
|
||||
; board_build.f_flash = 80000000L
|
||||
; board_build.flash_mode = qio
|
||||
|
||||
[env:esp32dev_16M]
|
||||
board = esp32dev
|
||||
platform = ${esp32_idf_V5.platform}
|
||||
platform_packages = ${esp32_idf_V5.platform_packages}
|
||||
custom_usermods = ${common.default_usermods}
|
||||
build_unflags = ${esp32_idf_V5.build_unflags}
|
||||
build_flags = ${common.build_flags} ${esp32_idf_V5.build_flags} -D WLED_RELEASE_NAME=\"ESP32_16M\" #-D WLED_DISABLE_BROWNOUT_DET
|
||||
-DARDUINO_USB_CDC_ON_BOOT=0 ;; this flag is mandatory for "classic ESP32" when building with arduino-esp32 >=2.0.3
|
||||
lib_deps = ${esp32_idf_V5.lib_deps}
|
||||
lib_ignore = ${esp32_idf_V5.lib_ignore}
|
||||
platform = ${esp32_idf_V4.platform}
|
||||
custom_usermods = audioreactive
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=\"ESP32_16M\" #-D WLED_DISABLE_BROWNOUT_DET
|
||||
lib_deps = ${esp32_idf_V4.lib_deps}
|
||||
monitor_filters = esp32_exception_decoder
|
||||
board_build.partitions = ${esp32.extreme_partitions}
|
||||
board_upload.flash_size = 16MB
|
||||
board_upload.maximum_size = 16777216
|
||||
board_build.f_flash = 80000000L
|
||||
board_build.flash_mode = dio
|
||||
|
||||
[env:esp32_eth]
|
||||
extends = esp32
|
||||
board = esp32-poe
|
||||
platform = ${esp32_idf_V5.platform}
|
||||
platform_packages = ${esp32_idf_V5.platform_packages}
|
||||
platform = ${esp32_idf_V4.platform}
|
||||
upload_speed = 921600
|
||||
custom_usermods = ${common.default_usermods}
|
||||
build_unflags = ${esp32_idf_V5.build_unflags}
|
||||
build_flags = ${common.build_flags} ${esp32_idf_V5.build_flags} -D WLED_RELEASE_NAME=\"ESP32_Ethernet\" -D RLYPIN=-1 -D WLED_USE_ETHERNET -D BTNPIN=-1
|
||||
-D SR_DMTYPE=-1 -D AUDIOPIN=-1 -D I2S_SDPIN=-1 -D I2S_WSPIN=-1 -D I2S_CKPIN=-1 -D MCLK_PIN=-1 ;; force AR to not allocate any PINs at startup
|
||||
-D DATA_PINS=4 ;; default led pin = 16 conflicts with pins used for ethernet
|
||||
; -D WLED_DISABLE_ESPNOW ;; ESP-NOW requires wifi, may crash with ethernet only => uncomment if your board uses ETH_CLOCK_GPIO0_OUT, ETH_CLOCK_GPIO16_OUT, ETH_CLOCK_GPIO17_OUT
|
||||
-DARDUINO_USB_CDC_ON_BOOT=0 ;; this flag is mandatory for "classic ESP32" when building with arduino-esp32 >=2.0.3
|
||||
custom_usermods = audioreactive
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = ${common.build_flags} ${esp32.build_flags} -D WLED_RELEASE_NAME=\"ESP32_Ethernet\" -D RLYPIN=-1 -D WLED_USE_ETHERNET -D BTNPIN=-1
|
||||
; -D WLED_DISABLE_ESPNOW ;; ESP-NOW requires wifi, may crash with ethernet only
|
||||
lib_deps = ${esp32_idf_V5.lib_deps}
|
||||
lib_ignore = ${esp32_idf_V5.lib_ignore}
|
||||
lib_deps = ${esp32.lib_deps}
|
||||
board_build.partitions = ${esp32.default_partitions}
|
||||
board_build.flash_mode = dio
|
||||
|
||||
[env:esp32_wrover]
|
||||
extends = esp32_idf_V5
|
||||
extends = esp32_idf_V4
|
||||
board = ttgo-t7-v14-mini32
|
||||
board_build.f_flash = 80000000L
|
||||
board_build.flash_mode = qio
|
||||
board_build.partitions = ${esp32.extended_partitions}
|
||||
custom_usermods = ${common.default_usermods}
|
||||
build_unflags = ${esp32_idf_V5.build_unflags}
|
||||
build_flags = ${common.build_flags} ${esp32_idf_V5.build_flags} -D WLED_RELEASE_NAME=\"ESP32_WROVER\"
|
||||
-DARDUINO_USB_CDC_ON_BOOT=0 ;; this flag is mandatory for "classic ESP32" when building with arduino-esp32 >=2.0.3
|
||||
custom_usermods = audioreactive
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=\"ESP32_WROVER\"
|
||||
-DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue ;; Older ESP32 (rev.<3) need a PSRAM fix (increases static RAM used) https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-guides/external-ram.html
|
||||
-D DATA_PINS=25
|
||||
lib_deps = ${esp32_idf_V5.lib_deps}
|
||||
lib_deps = ${esp32_idf_V4.lib_deps}
|
||||
|
||||
[env:esp32c3dev]
|
||||
extends = esp32c3
|
||||
platform = ${esp32c3.platform}
|
||||
framework = arduino
|
||||
board = esp32-c3-devkitm-1
|
||||
custom_usermods = audioreactive
|
||||
board_build.partitions = ${esp32.default_partitions}
|
||||
build_flags = ${common.build_flags} ${esp32c3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-C3\"
|
||||
-D WLED_WATCHDOG_TIMEOUT=0
|
||||
-DLOLIN_WIFI_FIX ; seems to work much better with this
|
||||
-DARDUINO_USB_CDC_ON_BOOT=1 ;; for virtual CDC USB
|
||||
;-DARDUINO_USB_CDC_ON_BOOT=0 ;; for serial-to-USB chip
|
||||
upload_speed = 460800
|
||||
build_unflags = ${esp32c3.build_unflags}
|
||||
build_unflags = ${common.build_unflags}
|
||||
lib_deps = ${esp32c3.lib_deps}
|
||||
board_build.flash_mode = dio ; safe default, required for OTA updates to 0.16 from older version which used dio (must match the bootloader!)
|
||||
|
||||
[env:esp32c3dev_qio]
|
||||
extends = env:esp32c3dev
|
||||
build_flags = ${common.build_flags} ${esp32c3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-C3-QIO\"
|
||||
board_build.flash_mode = qio ; qio is faster and works on almost all boards (some boards may use dio to get 2 extra pins)
|
||||
|
||||
[env:esp32s3dev_16MB_opi]
|
||||
;; ESP32-S3 development board, with 16MB FLASH and >= 8MB PSRAM (memory_type: qio_opi)
|
||||
extends = esp32s3
|
||||
board = esp32-s3-devkitc-1 ;; generic dev board; the next line adds PSRAM support
|
||||
board_build.arduino.memory_type = qio_opi ;; use with PSRAM: 8MB or 16MB
|
||||
platform = ${esp32s3.platform}
|
||||
platform_packages = ${esp32s3.platform_packages}
|
||||
upload_speed = 921600
|
||||
custom_usermods = ${common.default_usermods}
|
||||
build_unflags = ${esp32s3.build_unflags}
|
||||
custom_usermods = audioreactive
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_16MB_opi\"
|
||||
-D WLED_WATCHDOG_TIMEOUT=0
|
||||
-D CONFIG_LITTLEFS_FOR_IDF_3_2 -D WLED_WATCHDOG_TIMEOUT=0
|
||||
;-D ARDUINO_USB_CDC_ON_BOOT=0 ;; -D ARDUINO_USB_MODE=1 ;; for boards with serial-to-USB chip
|
||||
-D ARDUINO_USB_CDC_ON_BOOT=1 ;; -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB")
|
||||
-D ARDUINO_USB_CDC_ON_BOOT=1 -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB")
|
||||
-DBOARD_HAS_PSRAM
|
||||
lib_deps = ${esp32s3.lib_deps}
|
||||
lib_ignore = ${esp32s3.lib_ignore}
|
||||
board_build.partitions = ${esp32.extreme_partitions}
|
||||
board_upload.flash_size = 16MB
|
||||
board_upload.maximum_size = 16777216
|
||||
board_build.f_flash = 80000000L
|
||||
board_build.flash_mode = qio
|
||||
monitor_filters = esp32_exception_decoder
|
||||
|
||||
[env:esp32s3dev_8MB_opi]
|
||||
;; ESP32-S3 development board, with 8MB FLASH and >= 8MB PSRAM (memory_type: qio_opi)
|
||||
extends = esp32s3
|
||||
board = esp32-s3-devkitc-1 ;; generic dev board; the next line adds PSRAM support
|
||||
board_build.arduino.memory_type = qio_opi ;; use with PSRAM: 8MB or 16MB
|
||||
platform = ${esp32s3.platform}
|
||||
platform_packages = ${esp32s3.platform_packages}
|
||||
upload_speed = 921600
|
||||
custom_usermods = ${common.default_usermods}
|
||||
build_unflags = ${esp32s3.build_unflags}
|
||||
custom_usermods = audioreactive
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_8MB_opi\"
|
||||
-D WLED_WATCHDOG_TIMEOUT=0
|
||||
-D CONFIG_LITTLEFS_FOR_IDF_3_2 -D WLED_WATCHDOG_TIMEOUT=0
|
||||
;-D ARDUINO_USB_CDC_ON_BOOT=0 ;; -D ARDUINO_USB_MODE=1 ;; for boards with serial-to-USB chip
|
||||
-D ARDUINO_USB_CDC_ON_BOOT=1 ;; -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB")
|
||||
-D ARDUINO_USB_CDC_ON_BOOT=1 -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB")
|
||||
-DBOARD_HAS_PSRAM
|
||||
lib_deps = ${esp32s3.lib_deps}
|
||||
lib_ignore = ${esp32s3.lib_ignore}
|
||||
board_build.partitions = ${esp32.large_partitions}
|
||||
board_build.f_flash = 80000000L
|
||||
board_build.flash_mode = qio
|
||||
|
||||
[env:esp32s3dev_8MB_qspi]
|
||||
;; generic ESP32-S3 board with 8MB FLASH and PSRAM (memory_type: qio_qspi). Try this one if esp32s3dev_8MB_opi does not work on your board
|
||||
extends = env:esp32s3dev_8MB_opi
|
||||
board_build.arduino.memory_type = qio_qspi
|
||||
board_build.flash_mode = qio
|
||||
build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_8MB_qspi\"
|
||||
-D WLED_WATCHDOG_TIMEOUT=0
|
||||
;-D ARDUINO_USB_CDC_ON_BOOT=0 ;; -D ARDUINO_USB_MODE=1 ;; for boards with serial-to-USB chip
|
||||
-D ARDUINO_USB_CDC_ON_BOOT=1 ;; -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB")
|
||||
-DBOARD_HAS_PSRAM
|
||||
;; -DLOLIN_WIFI_FIX ;; uncomment if you have WiFi connectivity problems
|
||||
|
||||
[env:esp32s3dev_8MB_none]
|
||||
;; ESP32-S3 development board, 8MB FLASH, no PSRAM
|
||||
extends = esp32s3
|
||||
board = esp32-s3-devkitc-1 ;; generic dev board
|
||||
custom_usermods = audioreactive
|
||||
build_unflags = ${esp32s3.build_unflags} -DBOARD_HAS_PSRAM ;; make sure PSRAM support is removed
|
||||
build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_8MB_none\"
|
||||
-D WLED_WATCHDOG_TIMEOUT=0
|
||||
;-D ARDUINO_USB_CDC_ON_BOOT=0 ;; -D ARDUINO_USB_MODE=1 ;; for boards with serial-to-USB chip
|
||||
-D ARDUINO_USB_CDC_ON_BOOT=1 ;; -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB")
|
||||
board_build.f_flash = 80000000L
|
||||
board_build.flash_mode = qio
|
||||
monitor_filters = esp32_exception_decoder
|
||||
|
||||
[env:esp32S3_wroom2]
|
||||
;; For ESP32-S3 WROOM-2, a.k.a. ESP32-S3 DevKitC-1 v1.1
|
||||
;; with >= 16MB FLASH and >= 8MB PSRAM (memory_type: opi_opi)
|
||||
extends = esp32s3
|
||||
platform = ${esp32s3.platform}
|
||||
board = esp32s3camlcd ;; this is the only standard board with "opi_opi"
|
||||
board_build.arduino.memory_type = opi_opi
|
||||
upload_speed = 921600
|
||||
custom_usermods = ${common.default_usermods}
|
||||
build_unflags = ${esp32s3.build_unflags}
|
||||
custom_usermods = audioreactive
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_WROOM-2\"
|
||||
-D WLED_WATCHDOG_TIMEOUT=0
|
||||
-D CONFIG_LITTLEFS_FOR_IDF_3_2 -D WLED_WATCHDOG_TIMEOUT=0
|
||||
-D ARDUINO_USB_CDC_ON_BOOT=0 ;; -D ARDUINO_USB_MODE=1 ;; for boards with serial-to-USB chip
|
||||
;; -D ARDUINO_USB_CDC_ON_BOOT=1 ;; -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB")
|
||||
;; -D ARDUINO_USB_CDC_ON_BOOT=1 -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB")
|
||||
-DBOARD_HAS_PSRAM
|
||||
-D LEDPIN=38 -D DATA_PINS=38 ;; buildin WS2812b LED
|
||||
-D BTNPIN=0 -D RLYPIN=16 -D IRPIN=17 -D AUDIOPIN=-1
|
||||
;;-D WLED_DEBUG
|
||||
-D WLED_DEBUG
|
||||
-D SR_DMTYPE=1 -D I2S_SDPIN=13 -D I2S_CKPIN=14 -D I2S_WSPIN=15 -D MCLK_PIN=4 ;; I2S mic
|
||||
lib_deps = ${esp32s3.lib_deps}
|
||||
lib_ignore = ${esp32s3.lib_ignore}
|
||||
|
||||
board_build.partitions = ${esp32.extreme_partitions}
|
||||
board_upload.flash_size = 16MB
|
||||
board_upload.maximum_size = 16777216
|
||||
|
||||
[env:esp32S3_wroom2_32MB]
|
||||
;; For ESP32-S3 WROOM-2 with 32MB Flash, and >= 8MB PSRAM (memory_type: opi_opi)
|
||||
extends = env:esp32S3_wroom2
|
||||
build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_WROOM-2_32MB\"
|
||||
-D WLED_WATCHDOG_TIMEOUT=0
|
||||
-D ARDUINO_USB_CDC_ON_BOOT=0 ;; -D ARDUINO_USB_MODE=1 ;; for boards with serial-to-USB chip
|
||||
;; -D ARDUINO_USB_CDC_ON_BOOT=1 ;; -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB")
|
||||
-DBOARD_HAS_PSRAM
|
||||
-D LEDPIN=38 -D DATA_PINS=38 ;; buildin WS2812b LED
|
||||
-D BTNPIN=0 -D RLYPIN=16 -D IRPIN=17 -D AUDIOPIN=-1
|
||||
;;-D WLED_DEBUG
|
||||
-D SR_DMTYPE=1 -D I2S_SDPIN=13 -D I2S_CKPIN=14 -D I2S_WSPIN=15 -D MCLK_PIN=4 ;; I2S mic
|
||||
board_build.partitions = tools/WLED_ESP32_32MB.csv
|
||||
board_upload.flash_size = 32MB
|
||||
board_upload.maximum_size = 33554432
|
||||
monitor_filters = esp32_exception_decoder
|
||||
|
||||
[env:esp32s3_4M_qspi]
|
||||
;; ESP32-S3, with 4MB FLASH and <= 4MB PSRAM (memory_type: qio_qspi)
|
||||
extends = esp32s3
|
||||
board = lolin_s3_mini ;; -S3 mini, 4MB flash 2MB PSRAM
|
||||
platform = ${esp32s3.platform}
|
||||
upload_speed = 921600
|
||||
custom_usermods = ${common.default_usermods}
|
||||
build_unflags = ${esp32s3.build_unflags}
|
||||
custom_usermods = audioreactive
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_4M_qspi\"
|
||||
-DARDUINO_USB_CDC_ON_BOOT=1 ;; -DARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB")
|
||||
-DARDUINO_USB_CDC_ON_BOOT=1 -DARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB")
|
||||
-DBOARD_HAS_PSRAM
|
||||
-DLOLIN_WIFI_FIX ; seems to work much better with this
|
||||
-D WLED_WATCHDOG_TIMEOUT=0
|
||||
lib_deps = ${esp32s3.lib_deps}
|
||||
lib_ignore = ${esp32s3.lib_ignore}
|
||||
board_build.partitions = ${esp32.default_partitions}
|
||||
board_build.f_flash = 80000000L
|
||||
board_build.flash_mode = qio
|
||||
|
||||
[env:esp32s3_4M_none]
|
||||
;; ESP32-S3 with 4MB FLASH, no PSRAM
|
||||
extends = esp32s3
|
||||
board = esp32-s3-devkitc-1
|
||||
custom_usermods = audioreactive
|
||||
build_unflags = ${esp32s3.build_unflags} -D BOARD_HAS_PSRAM ;; make sure PSRAM support is removed
|
||||
build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_4M_none\"
|
||||
-DARDUINO_USB_CDC_ON_BOOT=1 ;; -DARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB")
|
||||
-DLOLIN_WIFI_FIX ; seems to work much better with this
|
||||
-D WLED_WATCHDOG_TIMEOUT=0
|
||||
board_build.partitions = ${esp32.default_partitions}
|
||||
board_build.f_flash = 80000000L
|
||||
board_build.flash_mode = qio
|
||||
|
||||
monitor_filters = esp32_exception_decoder
|
||||
|
||||
[env:lolin_s2_mini]
|
||||
extends = esp32s2
|
||||
platform = ${esp32s2.platform}
|
||||
board = lolin_s2_mini
|
||||
board_build.partitions = ${esp32.default_partitions}
|
||||
board_build.flash_mode = qio
|
||||
board_build.f_flash = 80000000L
|
||||
custom_usermods = ${common.default_usermods}
|
||||
build_unflags = ${esp32s2.build_unflags}
|
||||
custom_usermods = audioreactive
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = ${common.build_flags} ${esp32s2.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S2\"
|
||||
-DARDUINO_USB_CDC_ON_BOOT=1
|
||||
-DARDUINO_USB_MSC_ON_BOOT=0
|
||||
@@ -811,121 +611,16 @@ build_flags = ${common.build_flags} ${esp32s2.build_flags} -D WLED_RELEASE_NAME=
|
||||
-D HW_PIN_MISOSPI=9
|
||||
; -D STATUSLED=15
|
||||
lib_deps = ${esp32s2.lib_deps}
|
||||
lib_ignore = ${esp32s2.lib_ignore}
|
||||
|
||||
|
||||
[env:usermods]
|
||||
board = esp32dev
|
||||
platform = ${esp32_idf_V5.platform}
|
||||
platform_packages = ${esp32_idf_V5.platform_packages}
|
||||
build_unflags = ${esp32_idf_V5.build_unflags}
|
||||
build_flags = ${common.build_flags} ${esp32_idf_V5.build_flags} -D WLED_RELEASE_NAME=\"ESP32_USERMODS\"
|
||||
platform = ${esp32_idf_V4.platform}
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=\"ESP32_USERMODS\"
|
||||
-DTOUCH_CS=9
|
||||
lib_deps = ${esp32_idf_V5.lib_deps}
|
||||
lib_ignore = ${esp32_idf_V5.lib_ignore}
|
||||
lib_deps = ${esp32_idf_V4.lib_deps}
|
||||
monitor_filters = esp32_exception_decoder
|
||||
board_build.flash_mode = dio
|
||||
;custom_usermods = * ; Expands to all usermods in usermods folder
|
||||
custom_usermods = ; ToDO: fix usermods build once the main V5 build works without errors and warnings
|
||||
custom_usermods = * ; Expands to all usermods in usermods folder
|
||||
board_build.partitions = ${esp32.extreme_partitions} ; We're gonna need a bigger boat
|
||||
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Hub75 examples
|
||||
# ------------------------------------------------------------------------------
|
||||
# Note: some panels may experience ghosting with default full brightness. use -D WLED_HUB75_MAX_BRIGHTNESS=239 or lower to fix it.
|
||||
|
||||
[hub75]
|
||||
;; Shared values for all HUB75 build envs.
|
||||
|
||||
;; Core HUB75 flags - common to every HUB75 build
|
||||
build_flags =
|
||||
-D WLED_ENABLE_HUB75MATRIX -D NO_GFX
|
||||
-D NO_CIE1931 ;; disable driver-internal gamma correction
|
||||
-D WLED_DEBUG_BUS
|
||||
-D LED_TYPES=TYPE_HUB75MATRIX_HS
|
||||
; -D WLED_DEBUG
|
||||
|
||||
;; Default I2S mic pins disabled (HUB75 uses GPIOs that would otherwise clash).
|
||||
;; Envs that wire up a real mic should NOT include this and define I2S_*PIN themselves.
|
||||
i2s_disable_flags = -D SR_DMTYPE=1 -D I2S_SDPIN=-1 -D I2S_CKPIN=-1 -D I2S_WSPIN=-1 -D MCLK_PIN=-1 ;; Disable to prevent pin clash
|
||||
|
||||
;; Pinned HUB75 driver libraries
|
||||
lib_deps = https://github.com/mrfaptastic/ESP32-HUB75-MatrixPanel-DMA.git#f17fb7fe9d487e9643f919eb5aeedea8d9d1f8d7 ;; 3.0.14
|
||||
|
||||
;; Extra flags shared by all S3-based HUB75 builds
|
||||
s3_build_flags =
|
||||
-DARDUINO_USB_CDC_ON_BOOT=1 ;; -DARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB")
|
||||
-DBOARD_HAS_PSRAM
|
||||
-DLOLIN_WIFI_FIX ; seems to work much better with this (sets lower TX power)
|
||||
-D WLED_WATCHDOG_TIMEOUT=0
|
||||
-D S3_LCD_DIV_NUM=20 ;; Attempt to fix wifi performance issue when panel active with S3 chips
|
||||
|
||||
|
||||
[env:esp32dev_hub75]
|
||||
extends = env:esp32dev
|
||||
upload_speed = 921600
|
||||
build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} ${hub75.build_flags} ${hub75.i2s_disable_flags}
|
||||
-DARDUINO_USB_CDC_ON_BOOT=0 ;; this flag is mandatory for "classic ESP32" when building with arduino-esp32 >=2.0.3
|
||||
-D WLED_RELEASE_NAME=\"ESP32_HUB75\"
|
||||
lib_deps = ${esp32_idf_V4.lib_deps}
|
||||
${hub75.lib_deps}
|
||||
|
||||
[env:esp32dev_hub75_forum_pinout]
|
||||
extends = env:esp32dev_hub75
|
||||
build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} ${hub75.build_flags} ${hub75.i2s_disable_flags}
|
||||
-DARDUINO_USB_CDC_ON_BOOT=0
|
||||
-D WLED_RELEASE_NAME=\"ESP32_HUB75_forum_pinout\"
|
||||
-D ESP32_FORUM_PINOUT ;; enable for SmartMatrix default pins
|
||||
|
||||
[env:esp32s3dev_4MB_qspi_hub75]
|
||||
; HD-WF2 - NOTE: this board has NO PSRAM, so BOARD_HAS_PSRAM must not be set
|
||||
; (BOARD_HAS_PSRAM causes the DMA library to allocate only in SPIRAM, which fails without PSRAM)
|
||||
extends = env:esp32s3dev_8MB_qspi
|
||||
board_build.partitions = ${esp32.extended_partitions} ;; 1.65MB firmware, 700KB filesystem - only 4MB flash usable on this board
|
||||
build_unflags = ${esp32s3.build_unflags} -DBOARD_HAS_PSRAM
|
||||
build_flags = ${common.build_flags} ${esp32s3.build_flags} ${hub75.build_flags} ${hub75.s3_build_flags} ${hub75.i2s_disable_flags}
|
||||
-D WLED_RELEASE_NAME=\"ESP32-S3_HD-WF2\"
|
||||
-D HD_WF2_PINOUT ;; Huidu HD-WF2 specific GPIO wiring
|
||||
lib_deps = ${esp32s3.lib_deps}
|
||||
${hub75.lib_deps}
|
||||
|
||||
[env:adafruit_matrixportal_esp32s3]
|
||||
; ESP32-S3 processor, 8 MB flash, 2 MB of PSRAM, dedicated driver pins for HUB75
|
||||
extends = env:esp32s3dev_8MB_qspi
|
||||
board = adafruit_matrixportal_esp32s3_wled ; modified board definition: removed flash section that causes FS erase on upload
|
||||
build_flags = ${common.build_flags} ${esp32s3.build_flags} ${hub75.build_flags} ${hub75.s3_build_flags} ${hub75.i2s_disable_flags}
|
||||
-D WLED_RELEASE_NAME=\"ESP32-S3_Adafruit_Matrixportal\"
|
||||
-D ARDUINO_ADAFRUIT_MATRIXPORTAL_ESP32S3
|
||||
lib_deps = ${esp32s3.lib_deps}
|
||||
${hub75.lib_deps}
|
||||
;; board_build.partitions = tools/partitions-8MB_spiffs-tinyuf2.csv ;; supports adafruit UF2 bootloader
|
||||
|
||||
[env:waveshare_esp32s3_32MB_hub75]
|
||||
;; Waveshare ESP32-S3-RGB-Matrix (memory_type: opi_opi); see https://docs.waveshare.com/ESP32-S3-RGB-Matrix
|
||||
extends = env:esp32S3_wroom2_32MB
|
||||
monitor_filters = esp32_exception_decoder
|
||||
build_unflags = ${env:esp32S3_wroom2_32MB.build_unflags}
|
||||
-D WLED_RELEASE_NAME=\"ESP32-S3_WROOM-2_32MB\" ;; need to un-set the relese name before setting a new one
|
||||
build_flags = ${common.build_flags} ${esp32s3.build_flags} ${hub75.build_flags} ${hub75.s3_build_flags} ${hub75.i2s_disable_flags}
|
||||
-D WLED_RELEASE_NAME=\"ESP32-S3_Waveshare_HUB75\"
|
||||
-D WAVESHARE_S3_PINOUT
|
||||
; -D WLED_USE_SD_SPI
|
||||
; -D SD_PRINT_HOME_DIR
|
||||
; -D WLED_DEBUG
|
||||
lib_deps = ${esp32s3.lib_deps}
|
||||
${hub75.lib_deps}
|
||||
|
||||
[env:esp32s3dev_16MB_opi_hub75]
|
||||
;; MOONHUB HUB75 adapter board (lilygo T7-S3 with 16MB flash and octal PSRAM)
|
||||
extends = env:esp32s3dev_8MB_opi
|
||||
board = lilygo-t7-s3
|
||||
board_build.partitions = ${esp32.extreme_partitions} ;; for 16MB flash (overrides large_partitions for 8MB)
|
||||
;; Note: real I2S mic pins are wired here, so we do NOT include ${hub75.i2s_disable_flags}.
|
||||
build_flags = ${common.build_flags} ${esp32s3.build_flags} ${hub75.build_flags} ${hub75.s3_build_flags}
|
||||
-D WLED_RELEASE_NAME=\"ESP32-S3_16MB_opi_HUB75\"
|
||||
-D MOONHUB_S3_PINOUT ;; HUB75 pinout
|
||||
-D LEDPIN=14 -D BTNPIN=0 -D RLYPIN=15 -D IRPIN=-1 -D AUDIOPIN=-1 ;; defaults that avoid pin conflicts with HUB75
|
||||
-D SR_DMTYPE=1 -D I2S_SDPIN=10 -D I2S_CKPIN=11 -D I2S_WSPIN=12 -D MCLK_PIN=-1 ;; I2S mic
|
||||
lib_deps = ${esp32s3.lib_deps}
|
||||
${hub75.lib_deps}
|
||||
|
||||
+133
-141
@@ -5,7 +5,7 @@
|
||||
# Please visit documentation: https://docs.platformio.org/page/projectconf.html
|
||||
|
||||
[platformio]
|
||||
default_envs = WLED_generic8266_1M, esp32dev_dio80 # put the name(s) of your own build environment here. You can define as many as you need
|
||||
default_envs = WLED_generic8266_1M, esp32dev_V4_dio80 # put the name(s) of your own build environment here. You can define as many as you need
|
||||
|
||||
#----------
|
||||
# SAMPLE
|
||||
@@ -13,8 +13,8 @@ default_envs = WLED_generic8266_1M, esp32dev_dio80 # put the name(s) of your ow
|
||||
[env:WLED_generic8266_1M]
|
||||
extends = env:esp01_1m_full # when you want to extend the existing environment (define only updated options)
|
||||
; board = esp01_1m # uncomment when ou need different board
|
||||
; platform = ${esp8266.platform_wled_default} # uncomment and change when you want particular platform
|
||||
; platform_packages = ${esp8266.platform_packages}
|
||||
; platform = ${common.platform_wled_default} # uncomment and change when you want particular platform
|
||||
; platform_packages = ${common.platform_packages}
|
||||
; board_build.ldscript = ${common.ldscript_1m128k}
|
||||
; upload_speed = 921600 # fast upload speed (remove ';' if your board supports fast upload speed)
|
||||
# Sample libraries used for various usermods. Uncomment when using particular usermod.
|
||||
@@ -28,20 +28,13 @@ lib_deps = ${esp8266.lib_deps}
|
||||
; robtillaart/SHT85@~0.3.3
|
||||
; ;gmag11/QuickESPNow @ ~0.7.0 # will also load QuickDebug
|
||||
; https://github.com/blazoncek/QuickESPNow.git#optional-debug ;; exludes debug library
|
||||
; ${esp32.AR_lib_deps} ;; needed for USERMOD_AUDIOREACTIVE
|
||||
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP8266_generic_1M\"
|
||||
-D WLED_DISABLE_PARTICLESYSTEM1D -D WLED_DISABLE_PARTICLESYSTEM2D
|
||||
-D WLED_DISABLE_PIXELFORGE
|
||||
-D WLED_DISABLE_OTA -D WLED_DISABLE_2D
|
||||
build_flags = ${common.build_flags} ${esp8266.build_flags}
|
||||
;
|
||||
; *** To use the below defines/overrides, copy and paste each onto its own line just below build_flags in the section above.
|
||||
; *** Note: on adding custom usermods
|
||||
; custom_usermods entries are a separate key in the env section (not inside build_flags). Place them on their own line in the env.
|
||||
; Multiple usermods are separated by a space, the names can be found in the library.json file of the usermod.
|
||||
; Example: use the default usermods from the esp32dev env and add the Temperature and four_line_display_ALT usermods
|
||||
; custom_usermods = ${env:esp32dev.custom_usermods} Temperature four_line_display_ALT
|
||||
;
|
||||
;
|
||||
; Set a release name that may be used to distinguish required binary for flashing
|
||||
; -D WLED_RELEASE_NAME=\"ESP32_MULTI_USREMODS\"
|
||||
;
|
||||
@@ -55,7 +48,7 @@ build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=
|
||||
; -D WLED_DISABLE_MQTT
|
||||
; -D WLED_DISABLE_ADALIGHT
|
||||
; -D WLED_DISABLE_2D
|
||||
; -D WLED_DISABLE_PIXELFORGE
|
||||
; -D WLED_DISABLE_PXMAGIC
|
||||
; -D WLED_DISABLE_ESPNOW
|
||||
; -D WLED_DISABLE_BROWNOUT_DET
|
||||
;
|
||||
@@ -106,17 +99,17 @@ build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=
|
||||
; -D WLED_DEBUG_PORT=7868
|
||||
;
|
||||
; Use Autosave usermod and set it to do save after 90s
|
||||
; custom_usermods = ${env:esp32dev.custom_usermods} auto_save
|
||||
; -D USERMOD_AUTO_SAVE
|
||||
; -D AUTOSAVE_AFTER_SEC=90
|
||||
;
|
||||
; Use AHT10/AHT15/AHT20 usermod
|
||||
; custom_usermods = ${env:esp32dev.custom_usermods} AHT10_v2
|
||||
; -D USERMOD_AHT10
|
||||
;
|
||||
; Use INA226 usermod
|
||||
; custom_usermods = ${env:esp32dev.custom_usermods} INA226_v2
|
||||
; -D USERMOD_INA226
|
||||
;
|
||||
; Use 4 Line Display usermod with SPI display
|
||||
; custom_usermods = ${env:esp32dev.custom_usermods} four_line_display_ALT
|
||||
; -D USERMOD_FOUR_LINE_DISPLAY
|
||||
; -DFLD_SPI_DEFAULT
|
||||
; -D FLD_TYPE=SSD1306_SPI64
|
||||
; -D FLD_PIN_CLOCKSPI=14
|
||||
@@ -126,27 +119,29 @@ build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=
|
||||
; -D FLD_PIN_RESET=27
|
||||
;
|
||||
; Use Rotary encoder usermod (in conjunction with 4LD)
|
||||
; custom_usermods = ${env:esp32dev.custom_usermods} rotary_encoder_ui_ALT
|
||||
; -D USERMOD_ROTARY_ENCODER_UI
|
||||
; -D ENCODER_DT_PIN=5
|
||||
; -D ENCODER_CLK_PIN=18
|
||||
; -D ENCODER_SW_PIN=19
|
||||
;
|
||||
; Use Dallas DS18B20 temperature sensor usermod and configure it to use GPIO13
|
||||
; custom_usermods = ${env:esp32dev.custom_usermods} Temperature
|
||||
; -D USERMOD_DALLASTEMPERATURE
|
||||
; -D TEMPERATURE_PIN=13
|
||||
;
|
||||
; Use Multi Relay usermod and configure it to use 6 relays and appropriate GPIO
|
||||
; custom_usermods = ${env:esp32dev.custom_usermods} multi_relay
|
||||
; -D USERMOD_MULTI_RELAY
|
||||
; -D MULTI_RELAY_MAX_RELAYS=6
|
||||
; -D MULTI_RELAY_PINS=12,23,22,21,24,25
|
||||
;
|
||||
; Use PIR sensor usermod and configure it to use GPIO4 and timer of 60s
|
||||
; custom_usermods = ${env:esp32dev.custom_usermods} PIR_sensor_switch
|
||||
; -D USERMOD_PIRSWITCH
|
||||
; -D PIR_SENSOR_PIN=4 # use -1 to disable usermod
|
||||
; -D PIR_SENSOR_OFF_SEC=60
|
||||
; -D PIR_SENSOR_MAX_SENSORS=2 # max allowable sensors (uses OR logic for triggering)
|
||||
;
|
||||
; Use Audioreactive usermod and configure I2S microphone
|
||||
; ${esp32.AR_build_flags} ;; default flags required to properly configure ArduinoFFT
|
||||
; ;; don't forget to add ArduinoFFT to your libs_deps: ${esp32.AR_lib_deps}
|
||||
; -D AUDIOPIN=-1
|
||||
; -D DMTYPE=1 # 0-analog/disabled, 1-I2S generic, 2-ES7243, 3-SPH0645, 4-I2S+mclk, 5-I2S PDM
|
||||
; -D I2S_SDPIN=36
|
||||
@@ -154,12 +149,12 @@ build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=
|
||||
; -D I2S_CKPIN=19
|
||||
;
|
||||
; Use PWM fan usermod
|
||||
; custom_usermods = ${env:esp32dev.custom_usermods} PWM_fan
|
||||
; -D USERMOD_PWM_FAN
|
||||
; -D TACHO_PIN=33
|
||||
; -D PWM_PIN=32
|
||||
;
|
||||
; Use POV Display usermod
|
||||
; custom_usermods = ${env:esp32dev.custom_usermods} pov_display
|
||||
; -D USERMOD_POV_DISPLAY
|
||||
; Use built-in or custom LED as a status indicator (assumes LED is connected to GPIO16)
|
||||
; -D STATUSLED=16
|
||||
;
|
||||
@@ -185,14 +180,13 @@ build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=
|
||||
;
|
||||
; enable IR by setting remote type
|
||||
; -D IRTYPE=0 # 0 Remote disabled | 1 24-key RGB | 2 24-key with CT | 3 40-key blue | 4 40-key RGB | 5 21-key RGB | 6 6-key black | 7 9-key red | 8 JSON remote
|
||||
;
|
||||
;
|
||||
; use PSRAM on classic ESP32 rev.1 (rev.3 or above has no issues)
|
||||
; -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue # needed only for classic ESP32 rev.1
|
||||
;
|
||||
; configure I2C and SPI interface (for various hardware)
|
||||
; -D I2CSDAPIN=33 # initialise interface
|
||||
; -D I2CSCLPIN=35 # initialise interface
|
||||
; # HW_PIN_* informs the WebUI about default pins - don't initialise interface
|
||||
; -D HW_PIN_SCL=35
|
||||
; -D HW_PIN_SDA=33
|
||||
; -D HW_PIN_CLOCKSPI=7
|
||||
@@ -200,23 +194,6 @@ build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=
|
||||
; -D HW_PIN_MISOSPI=9
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Optional: build flags for speed, instead of optimising for size.
|
||||
# Add ${Speed_Flags.build_flags} / ${Speed_Flags.build_unflags} to your own env
|
||||
# in platformio_override.ini to opt in.
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
[Speed_Flags]
|
||||
build_unflags = -Os ;; to disable standard optimization for small size
|
||||
build_flags =
|
||||
-O2 ;; optimize for speed
|
||||
-free -fipa-pta ;; very useful, too
|
||||
;;-fsingle-precision-constant ;; makes all floating point literals "float" (default is "double")
|
||||
;;-funsafe-math-optimizations ;; less dangerous than -ffast-math; still allows the compiler to exploit FMA and reciprocals (up to 10% faster on -S3)
|
||||
# Important: we need to explicitly switch off some "-O2" optimizations
|
||||
-fno-jump-tables -fno-tree-switch-conversion ;; needed - firmware may crash otherwise
|
||||
-freorder-blocks -Wwrite-strings -fstrict-volatile-bitfields ;; needed - recommended by espressif
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# PRE-CONFIGURED DEVELOPMENT BOARDS AND CONTROLLERS
|
||||
@@ -224,60 +201,67 @@ build_flags =
|
||||
|
||||
[env:esp07]
|
||||
board = esp07
|
||||
platform = ${esp8266.platform_wled_default}
|
||||
platform_packages = ${esp8266.platform_packages}
|
||||
platform = ${common.platform_wled_default}
|
||||
platform_packages = ${common.platform_packages}
|
||||
board_build.ldscript = ${common.ldscript_4m1m}
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP8266_ESP07\" #-DWLED_DISABLE_2D
|
||||
-D WLED_DISABLE_PARTICLESYSTEM2D
|
||||
build_flags = ${common.build_flags} ${esp8266.build_flags}
|
||||
lib_deps = ${esp8266.lib_deps}
|
||||
|
||||
[env:d1_mini]
|
||||
board = d1_mini
|
||||
platform = ${esp8266.platform_wled_default}
|
||||
platform_packages = ${esp8266.platform_packages}
|
||||
platform = ${common.platform_wled_default}
|
||||
platform_packages = ${common.platform_packages}
|
||||
upload_speed = 921600
|
||||
board_build.ldscript = ${common.ldscript_4m1m}
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP8266_D1MINI\" #-DWLED_DISABLE_2D
|
||||
-D WLED_DISABLE_PARTICLESYSTEM2D
|
||||
build_flags = ${common.build_flags} ${esp8266.build_flags}
|
||||
lib_deps = ${esp8266.lib_deps}
|
||||
monitor_filters = esp8266_exception_decoder
|
||||
|
||||
[env:heltec_wifi_kit_8]
|
||||
board = d1_mini
|
||||
platform = ${esp8266.platform_wled_default}
|
||||
platform_packages = ${esp8266.platform_packages}
|
||||
platform = ${common.platform_wled_default}
|
||||
platform_packages = ${common.platform_packages}
|
||||
board_build.ldscript = ${common.ldscript_4m1m}
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP8266_HT_D1MINI\" #-DWLED_DISABLE_2D
|
||||
-D WLED_DISABLE_PARTICLESYSTEM2D
|
||||
build_flags = ${common.build_flags} ${esp8266.build_flags}
|
||||
lib_deps = ${esp8266.lib_deps}
|
||||
|
||||
[env:h803wf]
|
||||
board = d1_mini
|
||||
platform = ${esp8266.platform_wled_default}
|
||||
platform_packages = ${esp8266.platform_packages}
|
||||
platform = ${common.platform_wled_default}
|
||||
platform_packages = ${common.platform_packages}
|
||||
board_build.ldscript = ${common.ldscript_4m1m}
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = ${common.build_flags} ${esp8266.build_flags} -D DATA_PINS=1 -D WLED_DISABLE_INFRARED -D WLED_RELEASE_NAME=\"ESP8266_HT803WF\" #-DWLED_DISABLE_2D
|
||||
-D WLED_DISABLE_PARTICLESYSTEM2D
|
||||
build_flags = ${common.build_flags} ${esp8266.build_flags} -D DATA_PINS=1 -D WLED_DISABLE_INFRARED
|
||||
lib_deps = ${esp8266.lib_deps}
|
||||
|
||||
[env:esp32dev_qio80]
|
||||
extends = env:esp32dev # we want to extend the existing esp32dev environment (and define only updated options)
|
||||
board = esp32dev
|
||||
build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=\"ESP32_qio80\" #-D WLED_DISABLE_BROWNOUT_DET
|
||||
lib_deps = ${esp32_idf_V4.lib_deps}
|
||||
build_flags = ${common.build_flags} ${esp32.build_flags} #-D WLED_DISABLE_BROWNOUT_DET
|
||||
${esp32.AR_build_flags} ;; optional - includes USERMOD_AUDIOREACTIVE
|
||||
lib_deps = ${esp32.lib_deps}
|
||||
${esp32.AR_lib_deps} ;; needed for USERMOD_AUDIOREACTIVE
|
||||
monitor_filters = esp32_exception_decoder
|
||||
board_build.f_flash = 80000000L
|
||||
board_build.flash_mode = qio
|
||||
|
||||
[env:esp32dev_dio80]
|
||||
extends = env:esp32dev_qio80 # we want to extend the previous environment, to change flash speed
|
||||
build_unflags = ${env:esp32dev_qio80.build_unflags} -D WLED_RELEASE_NAME=\"ESP32_qio80\" # need to remove the previous WLED_RELEASE_NAME
|
||||
build_flags = ${env:esp32dev_qio80.build_flags} -D WLED_RELEASE_NAME=\"ESP32_dio80\" # ... and then we can set a new one
|
||||
board_build.flash_mode = dio # change flash mode to "dio", for boards that cannot not start with "qio" mode
|
||||
[env:esp32dev_V4_dio80]
|
||||
;; experimental ESP32 env using ESP-IDF V4.4.x
|
||||
;; Warning: this build environment is not stable!!
|
||||
;; please erase your device before installing.
|
||||
extends = esp32_idf_V4 # based on newer "esp-idf V4" platform environment
|
||||
board = esp32dev
|
||||
build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} #-D WLED_DISABLE_BROWNOUT_DET
|
||||
${esp32.AR_build_flags} ;; includes USERMOD_AUDIOREACTIVE
|
||||
lib_deps = ${esp32_idf_V4.lib_deps}
|
||||
${esp32.AR_lib_deps} ;; needed for USERMOD_AUDIOREACTIVE
|
||||
monitor_filters = esp32_exception_decoder
|
||||
board_build.partitions = ${esp32.default_partitions} ;; if you get errors about "out of program space", change this to ${esp32.extended_partitions} or even ${esp32.big_partitions}
|
||||
board_build.f_flash = 80000000L
|
||||
board_build.flash_mode = dio
|
||||
|
||||
[env:esp32s2_saola]
|
||||
extends = esp32s2
|
||||
@@ -287,51 +271,53 @@ platform_packages = ${esp32s2.platform_packages}
|
||||
framework = arduino
|
||||
board_build.flash_mode = qio
|
||||
upload_speed = 460800
|
||||
build_flags = ${common.build_flags} ${esp32s2.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S2_saola\"
|
||||
build_flags = ${common.build_flags} ${esp32s2.build_flags}
|
||||
;-DLOLIN_WIFI_FIX ;; try this in case Wifi does not work
|
||||
-DARDUINO_USB_CDC_ON_BOOT=1
|
||||
lib_deps = ${esp32s2.lib_deps}
|
||||
|
||||
[env:esp32s3dev_8MB_PSRAM_qspi]
|
||||
;; ESP32-TinyS3 development board, with 8MB FLASH and PSRAM (memory_type: qio_qspi)
|
||||
extends = env:esp32s3dev_8MB_PSRAM_opi
|
||||
;board = um_tinys3 ; -> needs workaround from https://github.com/wled-dev/WLED/pull/2905#issuecomment-1328049860
|
||||
board = esp32-s3-devkitc-1 ;; generic dev board; the next line adds PSRAM support
|
||||
board_build.arduino.memory_type = qio_qspi ;; use with PSRAM: 2MB or 4MB
|
||||
|
||||
[env:esp8285_4CH_MagicHome]
|
||||
board = esp8285
|
||||
platform = ${esp8266.platform_wled_default}
|
||||
platform_packages = ${esp8266.platform_packages}
|
||||
platform = ${common.platform_wled_default}
|
||||
platform_packages = ${common.platform_packages}
|
||||
board_build.ldscript = ${common.ldscript_1m128k}
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_DISABLE_OTA -D WLED_RELEASE_NAME=\"ESP8285_4CH_MagicHome\" #-DWLED_DISABLE_2D
|
||||
-D WLED_DISABLE_PARTICLESYSTEM2D
|
||||
build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_DISABLE_OTA
|
||||
lib_deps = ${esp8266.lib_deps}
|
||||
|
||||
[env:esp8285_H801]
|
||||
board = esp8285
|
||||
platform = ${esp8266.platform_wled_default}
|
||||
platform_packages = ${esp8266.platform_packages}
|
||||
platform = ${common.platform_wled_default}
|
||||
platform_packages = ${common.platform_packages}
|
||||
board_build.ldscript = ${common.ldscript_1m128k}
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_DISABLE_OTA -D WLED_RELEASE_NAME=\"ESP8285_H801\" #-DWLED_DISABLE_2D
|
||||
-D WLED_DISABLE_PARTICLESYSTEM2D
|
||||
build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_DISABLE_OTA
|
||||
lib_deps = ${esp8266.lib_deps}
|
||||
|
||||
[env:d1_mini_5CH_Shojo_PCB]
|
||||
board = d1_mini
|
||||
platform = ${esp8266.platform_wled_default}
|
||||
platform_packages = ${esp8266.platform_packages}
|
||||
platform = ${common.platform_wled_default}
|
||||
platform_packages = ${common.platform_packages}
|
||||
board_build.ldscript = ${common.ldscript_4m1m}
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_USE_SHOJO_PCB ;; NB: WLED_USE_SHOJO_PCB is not used anywhere in the source code. Not sure why its needed.
|
||||
-D WLED_RELEASE_NAME=\"ESP8266_5CH_Shojo_PCB\" #-DWLED_DISABLE_2D
|
||||
-D WLED_DISABLE_PARTICLESYSTEM2D
|
||||
lib_deps = ${esp8266.lib_deps}
|
||||
|
||||
[env:d1_mini_debug]
|
||||
board = d1_mini
|
||||
build_type = debug
|
||||
platform = ${esp8266.platform_wled_default}
|
||||
platform_packages = ${esp8266.platform_packages}
|
||||
platform = ${common.platform_wled_default}
|
||||
platform_packages = ${common.platform_packages}
|
||||
board_build.ldscript = ${common.ldscript_4m1m}
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = ${common.build_flags} ${esp8266.build_flags} ${common.debug_flags} -D WLED_RELEASE_NAME=\"ESP8266_D1MINI_DEBUG\" #-DWLED_DISABLE_2D
|
||||
-D WLED_DISABLE_PARTICLESYSTEM2D
|
||||
build_flags = ${common.build_flags} ${esp8266.build_flags} ${common.debug_flags}
|
||||
lib_deps = ${esp8266.lib_deps}
|
||||
|
||||
[env:d1_mini_ota]
|
||||
@@ -339,23 +325,20 @@ board = d1_mini
|
||||
upload_protocol = espota
|
||||
# exchange for your WLED IP
|
||||
upload_port = "10.10.1.27"
|
||||
platform = ${esp8266.platform_wled_default}
|
||||
platform_packages = ${esp8266.platform_packages}
|
||||
platform = ${common.platform_wled_default}
|
||||
platform_packages = ${common.platform_packages}
|
||||
board_build.ldscript = ${common.ldscript_4m1m}
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP8266_D1MINI_OTA\" #-DWLED_DISABLE_2D
|
||||
-D WLED_DISABLE_PARTICLESYSTEM2D
|
||||
build_flags = ${common.build_flags} ${esp8266.build_flags}
|
||||
lib_deps = ${esp8266.lib_deps}
|
||||
|
||||
[env:anavi_miracle_controller]
|
||||
board = d1_mini
|
||||
platform = ${esp8266.platform_wled_default}
|
||||
platform_packages = ${esp8266.platform_packages}
|
||||
platform = ${common.platform_wled_default}
|
||||
platform_packages = ${common.platform_packages}
|
||||
board_build.ldscript = ${common.ldscript_4m1m}
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = ${common.build_flags} ${esp8266.build_flags} -D DATA_PINS=12 -D IRPIN=-1 -D RLYPIN=2
|
||||
-D WLED_RELEASE_NAME=\"ESP8266_ANAVI_MIRACLE\" #-DWLED_DISABLE_2D
|
||||
-D WLED_DISABLE_PARTICLESYSTEM2D
|
||||
lib_deps = ${esp8266.lib_deps}
|
||||
|
||||
[env:esp32c3dev_2MB]
|
||||
@@ -366,7 +349,6 @@ platform = ${esp32c3.platform}
|
||||
platform_packages = ${esp32c3.platform_packages}
|
||||
board = esp32-c3-devkitm-1
|
||||
build_flags = ${common.build_flags} ${esp32c3.build_flags}
|
||||
-D WLED_RELEASE_NAME=\"ESP32-C3_2MB\"
|
||||
-D WLED_WATCHDOG_TIMEOUT=0
|
||||
-D WLED_DISABLE_OTA
|
||||
; -DARDUINO_USB_CDC_ON_BOOT=1 ;; for virtual CDC USB
|
||||
@@ -383,7 +365,6 @@ board_upload.maximum_size = 2097152
|
||||
extends = esp32 ;; use default esp32 platform
|
||||
board = esp32dev
|
||||
upload_speed = 460800
|
||||
custom_usermods = ${env:esp32dev.custom_usermods} Temperature four_line_display_ALT
|
||||
build_flags = ${common.build_flags} ${esp32.build_flags}
|
||||
-D WLED_RELEASE_NAME=\"ESP32_wemos_shield\"
|
||||
-D DATA_PINS=16
|
||||
@@ -391,8 +372,14 @@ build_flags = ${common.build_flags} ${esp32.build_flags}
|
||||
-D BTNPIN=17
|
||||
-D IRPIN=18
|
||||
-UWLED_USE_MY_CONFIG
|
||||
-D USERMOD_DALLASTEMPERATURE
|
||||
-D USERMOD_FOUR_LINE_DISPLAY
|
||||
-D TEMPERATURE_PIN=23
|
||||
${esp32.AR_build_flags} ;; includes USERMOD_AUDIOREACTIVE
|
||||
lib_deps = ${esp32.lib_deps}
|
||||
OneWire@~2.3.5 ;; needed for USERMOD_DALLASTEMPERATURE
|
||||
olikraus/U8g2 @ ^2.28.8 ;; needed for USERMOD_FOUR_LINE_DISPLAY
|
||||
${esp32.AR_lib_deps} ;; needed for USERMOD_AUDIOREACTIVE
|
||||
board_build.partitions = ${esp32.default_partitions}
|
||||
|
||||
[env:esp32_pico-D4]
|
||||
@@ -404,95 +391,88 @@ build_flags = ${common.build_flags} ${esp32.build_flags}
|
||||
-D WLED_DISABLE_ADALIGHT ;; no serial-to-USB chip on this board - better to disable serial protocols
|
||||
-D DATA_PINS=2,18 ;; LED pins
|
||||
-D RLYPIN=19 -D BTNPIN=0 -D IRPIN=-1 ;; no default pin for IR
|
||||
${esp32.AR_build_flags} ;; include USERMOD_AUDIOREACTIVE
|
||||
-D UM_AUDIOREACTIVE_ENABLE ;; enable AR by default
|
||||
;; Audioreactive settings for on-board microphone (ICS-43432)
|
||||
-D SR_DMTYPE=1 -D I2S_SDPIN=25 -D I2S_WSPIN=15 -D I2S_CKPIN=14
|
||||
-D SR_SQUELCH=5 -D SR_GAIN=30
|
||||
lib_deps = ${esp32.lib_deps}
|
||||
${esp32.AR_lib_deps} ;; needed for USERMOD_AUDIOREACTIVE
|
||||
board_build.partitions = ${esp32.default_partitions}
|
||||
board_build.f_flash = 80000000L
|
||||
|
||||
[env:m5atom]
|
||||
extends = env:esp32dev # we want to extend the existing esp32dev environment (and define only updated options)
|
||||
build_flags = ${common.build_flags} ${esp32.build_flags} -D DATA_PINS=27 -D BTNPIN=39
|
||||
-D WLED_RELEASE_NAME=\"ESP32_m5atom\"
|
||||
|
||||
[env:sp501e]
|
||||
board = esp_wroom_02
|
||||
platform = ${esp8266.platform_wled_default}
|
||||
platform = ${common.platform_wled_default}
|
||||
board_build.ldscript = ${common.ldscript_2m512k}
|
||||
build_flags = ${common.build_flags} ${esp8266.build_flags} -D DATA_PINS=3 -D BTNPIN=1
|
||||
-D WLED_RELEASE_NAME=\"ESP8266_sp501e\" -D WLED_DISABLE_PARTICLESYSTEM2D
|
||||
lib_deps = ${esp8266.lib_deps}
|
||||
|
||||
[env:sp511e]
|
||||
board = esp_wroom_02
|
||||
platform = ${esp8266.platform_wled_default}
|
||||
platform = ${common.platform_wled_default}
|
||||
board_build.ldscript = ${common.ldscript_2m512k}
|
||||
build_flags = ${common.build_flags} ${esp8266.build_flags} -D DATA_PINS=3 -D BTNPIN=2 -D IRPIN=5 -D WLED_MAX_BUTTONS=3
|
||||
-D WLED_RELEASE_NAME=\"ESP8266_sp511e\" -D WLED_DISABLE_PARTICLESYSTEM2D
|
||||
lib_deps = ${esp8266.lib_deps}
|
||||
|
||||
[env:Athom_RGBCW] ;7w and 5w(GU10) bulbs
|
||||
board = esp8285
|
||||
platform = ${esp8266.platform_wled_default}
|
||||
platform_packages = ${esp8266.platform_packages}
|
||||
platform = ${common.platform_wled_default}
|
||||
platform_packages = ${common.platform_packages}
|
||||
board_build.ldscript = ${common.ldscript_2m512k}
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = ${common.build_flags} ${esp8266.build_flags} -D BTNPIN=-1 -D RLYPIN=-1 -D DATA_PINS=4,12,14,13,5
|
||||
-D LED_TYPES=TYPE_ANALOG_5CH -D WLED_DISABLE_INFRARED -D WLED_MAX_CCT_BLEND=0
|
||||
-D WLED_RELEASE_NAME=\"ESP8285_Athom_RGBCW\" -D WLED_DISABLE_PARTICLESYSTEM2D
|
||||
-D LED_TYPES=TYPE_ANALOG_5CH -D WLED_DISABLE_INFRARED -D WLED_MAX_CCT_BLEND=0
|
||||
lib_deps = ${esp8266.lib_deps}
|
||||
|
||||
[env:Athom_15w_RGBCW] ;15w bulb
|
||||
board = esp8285
|
||||
platform = ${esp8266.platform_wled_default}
|
||||
platform_packages = ${esp8266.platform_packages}
|
||||
platform = ${common.platform_wled_default}
|
||||
platform_packages = ${common.platform_packages}
|
||||
board_build.ldscript = ${common.ldscript_2m512k}
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = ${common.build_flags} ${esp8266.build_flags} -D BTNPIN=-1 -D RLYPIN=-1 -D DATA_PINS=4,12,14,5,13
|
||||
-D LED_TYPES=TYPE_ANALOG_5CH -D WLED_DISABLE_INFRARED -D WLED_MAX_CCT_BLEND=0 -D WLED_USE_IC_CCT
|
||||
-D WLED_RELEASE_NAME=\"ESP8285_Athom_15W_RGBCW\" -D WLED_DISABLE_PARTICLESYSTEM2D
|
||||
-D LED_TYPES=TYPE_ANALOG_5CH -D WLED_DISABLE_INFRARED -D WLED_MAX_CCT_BLEND=0 -D WLED_USE_IC_CCT
|
||||
lib_deps = ${esp8266.lib_deps}
|
||||
|
||||
[env:Athom_3Pin_Controller] ;small controller with only data
|
||||
board = esp8285
|
||||
platform = ${esp8266.platform_wled_default}
|
||||
platform_packages = ${esp8266.platform_packages}
|
||||
platform = ${common.platform_wled_default}
|
||||
platform_packages = ${common.platform_packages}
|
||||
board_build.ldscript = ${common.ldscript_2m512k}
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = ${common.build_flags} ${esp8266.build_flags} -D BTNPIN=0 -D RLYPIN=-1 -D DATA_PINS=1 -D WLED_DISABLE_INFRARED
|
||||
-D WLED_RELEASE_NAME=\"ESP8285_Athom_3Pin\" -D WLED_DISABLE_PARTICLESYSTEM2D
|
||||
lib_deps = ${esp8266.lib_deps}
|
||||
|
||||
[env:Athom_4Pin_Controller] ; With clock and data interface
|
||||
board = esp8285
|
||||
platform = ${esp8266.platform_wled_default}
|
||||
platform_packages = ${esp8266.platform_packages}
|
||||
platform = ${common.platform_wled_default}
|
||||
platform_packages = ${common.platform_packages}
|
||||
board_build.ldscript = ${common.ldscript_2m512k}
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = ${common.build_flags} ${esp8266.build_flags} -D BTNPIN=0 -D RLYPIN=12 -D DATA_PINS=1 -D WLED_DISABLE_INFRARED
|
||||
-D WLED_RELEASE_NAME=\"ESP8285_Athom_4Pin\" -D WLED_DISABLE_PARTICLESYSTEM2D
|
||||
lib_deps = ${esp8266.lib_deps}
|
||||
|
||||
[env:Athom_5Pin_Controller] ;Analog light strip controller
|
||||
board = esp8285
|
||||
platform = ${esp8266.platform_wled_default}
|
||||
platform_packages = ${esp8266.platform_packages}
|
||||
platform = ${common.platform_wled_default}
|
||||
platform_packages = ${common.platform_packages}
|
||||
board_build.ldscript = ${common.ldscript_2m512k}
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = ${common.build_flags} ${esp8266.build_flags} -D BTNPIN=0 -D RLYPIN=-1 DATA_PINS=4,12,14,13 -D WLED_DISABLE_INFRARED
|
||||
-D WLED_RELEASE_NAME=\"ESP8285_Athom_5Pin\" -D WLED_DISABLE_PARTICLESYSTEM2D
|
||||
lib_deps = ${esp8266.lib_deps}
|
||||
|
||||
[env:MY9291]
|
||||
board = esp01_1m
|
||||
platform = ${esp8266.platform_wled_default}
|
||||
platform_packages = ${esp8266.platform_packages}
|
||||
platform = ${common.platform_wled_default}
|
||||
platform_packages = ${common.platform_packages}
|
||||
board_build.ldscript = ${common.ldscript_1m128k}
|
||||
build_unflags = ${common.build_unflags}
|
||||
custom_usermods = ${env:esp01_1m_full.custom_usermods} MY9291
|
||||
build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_DISABLE_OTA -D WLED_RELEASE_NAME=\"ESP8266_MY9291\" -D WLED_DISABLE_PARTICLESYSTEM2D
|
||||
build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_DISABLE_OTA -D USERMOD_MY9291
|
||||
lib_deps = ${esp8266.lib_deps}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
@@ -502,44 +482,56 @@ lib_deps = ${esp8266.lib_deps}
|
||||
|
||||
[env:codm-controller-0_6]
|
||||
board = esp_wroom_02
|
||||
platform = ${esp8266.platform_wled_default}
|
||||
platform_packages = ${esp8266.platform_packages}
|
||||
platform = ${common.platform_wled_default}
|
||||
platform_packages = ${common.platform_packages}
|
||||
board_build.ldscript = ${common.ldscript_2m512k}
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP8266_CODM06_2MB\" -D WLED_DISABLE_PARTICLESYSTEM2D
|
||||
build_flags = ${common.build_flags} ${esp8266.build_flags}
|
||||
lib_deps = ${esp8266.lib_deps}
|
||||
|
||||
[env:codm-controller-0_6-rev2]
|
||||
board = esp_wroom_02
|
||||
platform = ${esp8266.platform_wled_default}
|
||||
platform_packages = ${esp8266.platform_packages}
|
||||
platform = ${common.platform_wled_default}
|
||||
platform_packages = ${common.platform_packages}
|
||||
board_build.ldscript = ${common.ldscript_4m1m}
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP8266_CODM06R2_4MB\" -D WLED_DISABLE_PARTICLESYSTEM2D
|
||||
build_flags = ${common.build_flags} ${esp8266.build_flags}
|
||||
lib_deps = ${esp8266.lib_deps}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# EleksTube-IPS
|
||||
# See usermods/EleksTube_IPS/platformio_override.ini.sample
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
[env:elekstube_ips]
|
||||
extends = esp32 ;; use default esp32 platform
|
||||
board = esp32dev
|
||||
upload_speed = 921600
|
||||
custom_usermods = ${env:esp32dev.custom_usermods} RTC EleksTube_IPS
|
||||
build_flags = ${common.build_flags} ${esp32.build_flags} -D WLED_DISABLE_BROWNOUT_DET -D WLED_DISABLE_INFRARED
|
||||
-D DATA_PINS=12
|
||||
-D RLYPIN=27
|
||||
-D BTNPIN=34
|
||||
-D PIXEL_COUNTS=6
|
||||
# Display config
|
||||
-D ST7789_DRIVER
|
||||
-D TFT_WIDTH=135
|
||||
-D TFT_HEIGHT=240
|
||||
-D CGRAM_OFFSET
|
||||
-D TFT_SDA_READ
|
||||
-D TFT_MOSI=23
|
||||
-D TFT_SCLK=18
|
||||
-D TFT_DC=25
|
||||
-D TFT_RST=26
|
||||
-D SPI_FREQUENCY=40000000
|
||||
-D USER_SETUP_LOADED
|
||||
monitor_filters = esp32_exception_decoder
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Usermod examples
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# 433MHz RF remote example: see usermods/usermod_v2_RF433/platformio_override.ini.sample
|
||||
|
||||
# External usermod from a git repository.
|
||||
# The library's `library.json` must include `"build": {"libArchive": false}`.
|
||||
# The name PlatformIO assigns is taken from the library's `library.json` "name" field.
|
||||
# If that name doesn't match the repo name in the URL, use the "LibName = URL" form
|
||||
# shown in the commented-out line below to supply the name explicitly.
|
||||
[env:esp32dev_external_usermod]
|
||||
# 433MHz RF remote example for esp32dev
|
||||
[env:esp32dev_usermod_RF433]
|
||||
extends = env:esp32dev
|
||||
custom_usermods =
|
||||
${env:esp32dev.custom_usermods}
|
||||
https://github.com/wled/wled-usermod-example.git#main
|
||||
|
||||
|
||||
|
||||
build_flags = ${env:esp32dev.build_flags} -D USERMOD_RF433
|
||||
lib_deps = ${env:esp32dev.lib_deps}
|
||||
sui77/rc-switch @ 2.6.4
|
||||
|
||||
@@ -7,97 +7,81 @@
|
||||
<a href="https://kno.wled.ge"><img src="https://img.shields.io/badge/quick_start-wiki-blue.svg?style=flat-square"></a>
|
||||
<a href="https://github.com/Aircoookie/WLED-App"><img src="https://img.shields.io/badge/app-wled-blue.svg?style=flat-square"></a>
|
||||
<a href="https://gitpod.io/#https://github.com/wled-dev/WLED"><img src="https://img.shields.io/badge/Gitpod-ready--to--code-blue?style=flat-square&logo=gitpod"></a>
|
||||
</p>
|
||||
|
||||
</p>
|
||||
|
||||
# Welcome to WLED! ✨
|
||||
|
||||
A fast and feature-rich firmware for ESP32 microcontrollers to control addressable LEDs — from simple strips to large 2D matrices and HUB75 panels.
|
||||
A fast and feature-rich implementation of an ESP32 and ESP8266 webserver to control NeoPixel (WS2812B, WS2811, SK6812) LEDs or also SPI based chipsets like the WS2801 and APA102!
|
||||
|
||||
Originally created by [Aircoookie](https://github.com/Aircoookie), now maintained by a community of contributors.
|
||||
Originally created by [Aircoookie](https://github.com/Aircoookie)
|
||||
|
||||
## ⚙️ Features
|
||||
- WS2812FX library with more than 100 special effects
|
||||
- FastLED noise effects and 50 palettes
|
||||
- Modern UI with color, effect and segment controls
|
||||
- Segments to set different effects and colors to user defined parts of the LED string
|
||||
- Settings page - configuration via the network
|
||||
- Access Point and station mode - automatic failsafe AP
|
||||
- [Up to 10 LED outputs](https://kno.wled.ge/features/multi-strip/#esp32) per instance
|
||||
- Support for RGBW strips
|
||||
- Up to 250 user presets to save and load colors/effects easily, supports cycling through them.
|
||||
- Presets can be used to automatically execute API calls
|
||||
- Nightlight function (gradually dims down)
|
||||
- Full OTA software updateability (HTTP + ArduinoOTA), password protectable
|
||||
- Configurable analog clock (Cronixie, 7-segment and EleksTube IPS clock support via usermods)
|
||||
- Configurable Auto Brightness limit for safe operation
|
||||
- Filesystem-based config for easier backup of presets and settings
|
||||
|
||||
### Effects & Visuals
|
||||
- [**200+ built-in effects**](https://kno.wled.ge/features/effects/) including classic animations, audio-reactive, and 2D/matrix effects
|
||||
- [50+ color palettes](https://kno.wled.ge/features/palettes/) plus a built-in **custom palette editor** (PixelForge)
|
||||
- [**2D LED matrix support**](https://kno.wled.ge/advanced/mapping/) with dedicated 2D effects and flexible panel mapping
|
||||
- [**HUB75 RGB matrix panel support**](https://kno.wled.ge/advanced/HUB75/) (ESP32)
|
||||
- [**AudioReactive**](https://kno.wled.ge/advanced/audio-reactive/) effects — included by default, responding to sound via microphone, line-in, or network audio source
|
||||
- Effect blending for smooth transitions between animations
|
||||
- Antialiased drawing functions for smooth graphics
|
||||
|
||||
### Segments & Control
|
||||
- [**Segments**](https://kno.wled.ge/features/segments/) — apply different effects, colors and palettes to independent parts of your LED setup simultaneously
|
||||
- Up to **250 presets** to save and recall colors, effects and segment configurations — supports [playlists](https://kno.wled.ge/features/presets/) for automated cycling
|
||||
- Nightlight function with configurable dimming curve
|
||||
- Configurable **Auto Brightness Limiter** (per output) for safe operation
|
||||
|
||||
### Hardware Support
|
||||
- **ESP32** (all variants: original, S2, S3, C3)
|
||||
- [**Up to 17 LED outputs**](https://kno.wled.ge/features/multi-strip/) on ESP32 using parallel I2S + RMT
|
||||
- [Addressable LED support](https://kno.wled.ge/basics/compatible-led-strips/): WS2812B, WS2811, WS2815, SK6812, WS2805, TM1914, APA102, WS2801, LPD8806, and many more
|
||||
- RGBW, [RGB+CCT](https://kno.wled.ge/features/cct/) and white-only strips
|
||||
- PWM outputs for analog LEDs and dimmers
|
||||
- [**Ethernet** support](https://kno.wled.ge/features/ethernet-lan/) for a wide range of boards (QuinLED, LILYGO, Olimex, and more)
|
||||
- Filesystem-based config for easy backup and restore of presets and settings
|
||||
- Full OTA firmware updates (HTTP + ArduinoOTA), password-protectable
|
||||
|
||||
### Connectivity & Integrations
|
||||
- **WLED app** for [Android](https://play.google.com/store/apps/details?id=ca.cgagnier.wlednativeandroid) and [iOS](https://apps.apple.com/gb/app/wled-native/id6446207239)
|
||||
- [JSON](https://kno.wled.ge/interfaces/json-api/) and [HTTP request](https://kno.wled.ge/interfaces/http-api/) APIs
|
||||
- **Multi-WiFi** — connect to up to 3 networks with automatic AP fallback
|
||||
- **ESP-NOW** wireless sync between devices (no WiFi router required)
|
||||
- [**MQTT**](https://kno.wled.ge/interfaces/mqtt/) with Home Assistant discovery
|
||||
- [**E1.31, Art-Net**](https://kno.wled.ge/interfaces/e1.31-dmx/), [DDP](https://kno.wled.ge/interfaces/ddp/) and [TPM2.net](https://kno.wled.ge/interfaces/udp-realtime/) for DMX/professional lighting control
|
||||
- [UDP realtime sync](https://kno.wled.ge/interfaces/udp-notifier/) across multiple WLED devices
|
||||
- Alexa voice control (on/off, brightness, color)
|
||||
- [Philips Hue sync](https://kno.wled.ge/interfaces/philips-hue/)
|
||||
- [diyHue](https://github.com/diyhue/diyHue) and [Hyperion](https://github.com/hyperion-project/hyperion.ng) integration
|
||||
- [Adalight / TPM2](https://kno.wled.ge/interfaces/serial/) (PC ambilight via serial)
|
||||
- [Infrared remote control](https://kno.wled.ge/interfaces/infrared/) (24-key RGB, receiver required)
|
||||
- Timers and schedules (NTP time sync, full timezone and DST support)
|
||||
|
||||
### Developer-Friendly
|
||||
- **Usermod system** — extend WLED with community or custom modules without modifying core code
|
||||
- Large and active [usermod library](https://kno.wled.ge/advanced/community-usermods/) including AudioReactive, temperature sensors, rotary encoders, displays, and much more
|
||||
- Well-documented [JSON API](https://kno.wled.ge/interfaces/json-api/)
|
||||
- Licensed under the **EUPL v1.2**
|
||||
## 💡 Supported light control interfaces
|
||||
- WLED app for [Android](https://play.google.com/store/apps/details?id=ca.cgagnier.wlednativeandroid) and [iOS](https://apps.apple.com/gb/app/wled-native/id6446207239)
|
||||
- JSON and HTTP request APIs
|
||||
- MQTT
|
||||
- E1.31, Art-Net, DDP and TPM2.net
|
||||
- [diyHue](https://github.com/diyhue/diyHue) (Wled is supported by diyHue, including Hue Sync Entertainment under udp. Thanks to [Gregory Mallios](https://github.com/gmallios))
|
||||
- [Hyperion](https://github.com/hyperion-project/hyperion.ng)
|
||||
- UDP realtime
|
||||
- Alexa voice control (including dimming and color)
|
||||
- Sync to Philips hue lights
|
||||
- Adalight (PC ambilight via serial) and TPM2
|
||||
- Sync color of multiple WLED devices (UDP notifier)
|
||||
- Infrared remotes (24-key RGB, receiver required)
|
||||
- Simple timers/schedules (time from NTP, timezones/DST supported)
|
||||
|
||||
## 📲 Quick start guide and documentation
|
||||
|
||||
See the [documentation at kno.wled.ge](https://kno.wled.ge)!
|
||||
See the [documentation on our official site](https://kno.wled.ge)!
|
||||
|
||||
[Tutorials and getting-started guides](https://kno.wled.ge/basics/tutorials/) to help you get your project running quickly.
|
||||
[On this page](https://kno.wled.ge/basics/tutorials/) you can find excellent tutorials and tools to help you get your new project up and running!
|
||||
|
||||
## 🖼️ User interface
|
||||
|
||||
<img src="/images/macbook-pro-space-gray-on-the-wooden-table.jpg" width="50%"><img src="/images/walking-with-iphone-x.jpg" width="50%">
|
||||
|
||||
## 💾 Compatible hardware
|
||||
|
||||
See the [compatible hardware list](https://kno.wled.ge/basics/compatible-hardware) on the wiki.
|
||||
See [here](https://kno.wled.ge/basics/compatible-hardware)!
|
||||
|
||||
## ✌️ Other
|
||||
|
||||
Licensed under the [EUPL v1.2](https://raw.githubusercontent.com/wled-dev/WLED/main/LICENSE).
|
||||
Credits to all [contributors](https://kno.wled.ge/about/contributors/)!
|
||||
CORS proxy by [Corsfix](https://corsfix.com/).
|
||||
Licensed under the EUPL v1.2 license
|
||||
Credits [here](https://kno.wled.ge/about/contributors/)!
|
||||
CORS proxy by [Corsfix](https://corsfix.com/)
|
||||
|
||||
Join the Discord server to discuss everything about WLED!
|
||||
|
||||
<a href="https://discord.gg/QAh7wJHrRM"><img src="https://discordapp.com/api/guilds/473448917040758787/widget.png?style=banner2" width="25%"></a>
|
||||
|
||||
Check out the WLED [Discourse forum](https://wled.discourse.group)!
|
||||
Check out the WLED [Discourse forum](https://wled.discourse.group)!
|
||||
|
||||
If you'd like to reach the original creator privately: [dev.aircoookie@gmail.com](mailto:dev.aircoookie@gmail.com).
|
||||
You can also send me mails to [dev.aircoookie@gmail.com](mailto:dev.aircoookie@gmail.com), but please, only do so if you want to talk to me privately.
|
||||
|
||||
If WLED brightens up your day, you can [send a gift to Aircoookie via PayPal](https://paypal.me/aircoookie).
|
||||
If WLED really brightens up your day, you can [](https://paypal.me/aircoookie)
|
||||
|
||||
---
|
||||
|
||||
*Disclaimer:*
|
||||
*Disclaimer:*
|
||||
|
||||
If you are prone to photosensitive epilepsy, we recommend you do **not** use this software.
|
||||
If you still want to try, avoid strobe, lightning or noise modes and high effect speed settings.
|
||||
If you are prone to photosensitive epilepsy, we recommended you do **not** use this software.
|
||||
If you still want to try, don't use strobe, lighting or noise modes or high effect speed settings.
|
||||
|
||||
As per the EUPL license, I assume no liability for any damage to you or any other person or equipment.
|
||||
|
||||
As per the EUPL license, no liability is assumed for any damage to you or any other person or equipment.
|
||||
|
||||
+14
-12
@@ -1,20 +1,20 @@
|
||||
#
|
||||
# This file is autogenerated by pip-compile with Python 3.13
|
||||
# This file is autogenerated by pip-compile with Python 3.11
|
||||
# by the following command:
|
||||
#
|
||||
# pip-compile
|
||||
# pip-compile requirements.in
|
||||
#
|
||||
ajsonrpc==1.2.0
|
||||
# via platformio
|
||||
anyio==4.10.0
|
||||
anyio==4.8.0
|
||||
# via starlette
|
||||
bottle==0.13.4
|
||||
bottle==0.13.2
|
||||
# via platformio
|
||||
certifi==2025.8.3
|
||||
certifi==2025.1.31
|
||||
# via requests
|
||||
charset-normalizer==3.4.3
|
||||
charset-normalizer==3.4.1
|
||||
# via requests
|
||||
click==8.1.7
|
||||
click==8.1.8
|
||||
# via
|
||||
# platformio
|
||||
# uvicorn
|
||||
@@ -30,27 +30,29 @@ idna==3.10
|
||||
# requests
|
||||
marshmallow==3.26.1
|
||||
# via platformio
|
||||
packaging==25.0
|
||||
packaging==24.2
|
||||
# via marshmallow
|
||||
platformio==6.1.18
|
||||
platformio==6.1.17
|
||||
# via -r requirements.in
|
||||
pyelftools==0.32
|
||||
# via platformio
|
||||
pyserial==3.5
|
||||
# via platformio
|
||||
requests==2.33.0
|
||||
requests==2.32.4
|
||||
# via platformio
|
||||
semantic-version==2.10.0
|
||||
# via platformio
|
||||
sniffio==1.3.1
|
||||
# via anyio
|
||||
starlette==0.46.2
|
||||
starlette==0.45.3
|
||||
# via platformio
|
||||
tabulate==0.9.0
|
||||
# via platformio
|
||||
typing-extensions==4.12.2
|
||||
# via anyio
|
||||
urllib3==2.5.0
|
||||
# via requests
|
||||
uvicorn==0.34.3
|
||||
uvicorn==0.34.0
|
||||
# via platformio
|
||||
wsproto==1.2.0
|
||||
# via platformio
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
# Name, Type, SubType, Offset, Size, Flags
|
||||
nvs, data, nvs, 0x9000, 0x5000,
|
||||
otadata, data, ota, 0xe000, 0x2000,
|
||||
app0, app, ota_0, 0x10000, 0x300000,
|
||||
app1, app, ota_1, 0x310000,0x300000,
|
||||
spiffs, data, spiffs, 0x610000,0x19E0000,
|
||||
coredump, data, coredump,,64K
|
||||
|
+6
-12
@@ -119,7 +119,7 @@ describe('Script', () => {
|
||||
|
||||
async function checkIfFileWasNewlyCreated(file) {
|
||||
const modifiedTime = fs.statSync(file).mtimeMs;
|
||||
assert(Date.now() - modifiedTime < 850, file + ' was not modified');
|
||||
assert(Date.now() - modifiedTime < 500, file + ' was not modified');
|
||||
}
|
||||
|
||||
async function testFileModification(sourceFilePath, resultFile) {
|
||||
@@ -129,7 +129,7 @@ describe('Script', () => {
|
||||
// modify file
|
||||
fs.appendFileSync(sourceFilePath, ' ');
|
||||
// delay for 1 second to ensure the modified time is different
|
||||
await new Promise(resolve => setTimeout(resolve, 1400));
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
// run script cdata.js again and wait for it to finish
|
||||
await execPromise('node tools/cdata.js');
|
||||
@@ -175,19 +175,13 @@ describe('Script', () => {
|
||||
});
|
||||
|
||||
it('a settings file changes', async () => {
|
||||
await testFileModification(path.join(dataPath, 'settings_leds.htm'), 'html_settings.h');
|
||||
await testFileModification(path.join(dataPath, 'settings_leds.htm'), 'html_ui.h');
|
||||
});
|
||||
|
||||
it('common.js changes', async () => {
|
||||
await testFileModification(path.join(dataPath, 'common.js'), 'html_settings.h');
|
||||
it('the favicon changes', async () => {
|
||||
await testFileModification(path.join(dataPath, 'favicon.ico'), 'html_ui.h');
|
||||
});
|
||||
|
||||
// this testcase currently fails - might be due to npm updates (maybe "faking" a favicon.ico change is harder now), or a real regression
|
||||
// see https://github.com/wled/WLED/issues/5581
|
||||
// it('the favicon changes', async () => {
|
||||
// await testFileModification(path.join(dataPath, 'favicon.ico'), 'html_other.h');
|
||||
// });
|
||||
|
||||
it('cdata.js changes', async () => {
|
||||
await testFileModification('tools/cdata.js', 'html_ui.h');
|
||||
});
|
||||
@@ -215,4 +209,4 @@ describe('Script', () => {
|
||||
assert(secondRunTime < firstRunTime / 2, 'html_*.h files were rebuilt');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
+11
-75
@@ -26,7 +26,7 @@ const packageJson = require("../package.json");
|
||||
// Export functions for testing
|
||||
module.exports = { isFileNewerThan, isAnyFileInFolderNewerThan };
|
||||
|
||||
const output = ["wled00/html_ui.h", "wled00/html_pixart.h", "wled00/html_cpal.h", "wled00/html_edit.h", "wled00/html_pxmagic.h", "wled00/html_pixelforge.h", "wled00/html_settings.h", "wled00/html_other.h", "wled00/js_iro.h", "wled00/js_omggif.h"]
|
||||
const output = ["wled00/html_ui.h", "wled00/html_pixart.h", "wled00/html_cpal.h", "wled00/html_pxmagic.h", "wled00/html_settings.h", "wled00/html_other.h"]
|
||||
|
||||
// \x1b[34m is blue, \x1b[36m is cyan, \x1b[0m is reset
|
||||
const wledBanner = `
|
||||
@@ -38,11 +38,6 @@ const wledBanner = `
|
||||
\t\t\x1b[36m build script for web UI
|
||||
\x1b[0m`;
|
||||
|
||||
// Generate build timestamp as UNIX timestamp (seconds since epoch)
|
||||
function generateBuildTime() {
|
||||
return Math.floor(Date.now() / 1000);
|
||||
}
|
||||
|
||||
const singleHeader = `/*
|
||||
* Binary array for the Web UI.
|
||||
* gzip is used for smaller size and improved speeds.
|
||||
@@ -50,9 +45,6 @@ const singleHeader = `/*
|
||||
* Please see https://kno.wled.ge/advanced/custom-features/#changing-web-ui
|
||||
* to find out how to easily modify the web UI source!
|
||||
*/
|
||||
|
||||
// Automatically generated build time for cache busting (UNIX timestamp)
|
||||
#define WEB_BUILD_TIME ${generateBuildTime()}
|
||||
|
||||
`;
|
||||
|
||||
@@ -134,13 +126,12 @@ async function minify(str, type = "plain") {
|
||||
throw new Error("Unknown filter: " + type);
|
||||
}
|
||||
|
||||
async function writeHtmlGzipped(sourceFile, resultFile, page, inlineCss = true) {
|
||||
async function writeHtmlGzipped(sourceFile, resultFile, page) {
|
||||
console.info("Reading " + sourceFile);
|
||||
inline.html({
|
||||
fileContent: fs.readFileSync(sourceFile, "utf8"),
|
||||
relativeTo: path.dirname(sourceFile),
|
||||
strict: inlineCss, // when not inlining css, ignore errors (enables linking style.css from subfolder htm files)
|
||||
stylesheets: inlineCss // when true (default), css is inlined
|
||||
strict: true,
|
||||
},
|
||||
async function (error, html) {
|
||||
if (error) throw error;
|
||||
@@ -152,7 +143,7 @@ async function writeHtmlGzipped(sourceFile, resultFile, page, inlineCss = true)
|
||||
console.info("Minified and compressed " + sourceFile + " from " + originalLength + " to " + result.length + " bytes");
|
||||
const array = hexdump(result);
|
||||
let src = singleHeader;
|
||||
src += `const uint16_t PAGE_${page}_length = ${result.length};\n`;
|
||||
src += `const uint16_t PAGE_${page}_L = ${result.length};\n`;
|
||||
src += `const uint8_t PAGE_${page}[] PROGMEM = {\n${array}\n};\n\n`;
|
||||
console.info("Writing " + resultFile);
|
||||
fs.writeFileSync(resultFile, src);
|
||||
@@ -253,63 +244,8 @@ if (isAlreadyBuilt("wled00/data") && process.argv[2] !== '--force' && process.ar
|
||||
|
||||
writeHtmlGzipped("wled00/data/index.htm", "wled00/html_ui.h", 'index');
|
||||
writeHtmlGzipped("wled00/data/pixart/pixart.htm", "wled00/html_pixart.h", 'pixart');
|
||||
writeHtmlGzipped("wled00/data/cpal/cpal.htm", "wled00/html_cpal.h", 'cpal');
|
||||
writeHtmlGzipped("wled00/data/pxmagic/pxmagic.htm", "wled00/html_pxmagic.h", 'pxmagic');
|
||||
writeHtmlGzipped("wled00/data/pixelforge/pixelforge.htm", "wled00/html_pixelforge.h", 'pixelforge', false); // do not inline css
|
||||
//writeHtmlGzipped("wled00/data/edit.htm", "wled00/html_edit.h", 'edit');
|
||||
|
||||
writeChunks(
|
||||
"wled00/data/",
|
||||
[
|
||||
{
|
||||
file: "iro.js",
|
||||
name: "JS_iro",
|
||||
method: "gzip",
|
||||
filter: "plain", // no minification, it is already minified
|
||||
mangle: (s) => s.replace(/^\/\*![\s\S]*?\*\//, '') // remove license comment at the top
|
||||
}
|
||||
],
|
||||
"wled00/js_iro.h"
|
||||
);
|
||||
|
||||
writeChunks(
|
||||
"wled00/data/pixelforge",
|
||||
[
|
||||
{
|
||||
file: "omggif.js",
|
||||
name: "JS_omggif",
|
||||
method: "gzip",
|
||||
filter: "js-minify",
|
||||
mangle: (s) => s.replace(/^\/\*![\s\S]*?\*\//, '') // remove license comment at the top
|
||||
}
|
||||
],
|
||||
"wled00/js_omggif.h"
|
||||
);
|
||||
|
||||
writeChunks(
|
||||
"wled00/data",
|
||||
[
|
||||
{
|
||||
file: "edit.htm",
|
||||
name: "PAGE_edit",
|
||||
method: "gzip",
|
||||
filter: "html-minify"
|
||||
}
|
||||
],
|
||||
"wled00/html_edit.h"
|
||||
);
|
||||
|
||||
writeChunks(
|
||||
"wled00/data/cpal",
|
||||
[
|
||||
{
|
||||
file: "cpal.htm",
|
||||
name: "PAGE_cpal",
|
||||
method: "gzip",
|
||||
filter: "html-minify"
|
||||
}
|
||||
],
|
||||
"wled00/html_cpal.h"
|
||||
);
|
||||
|
||||
writeChunks(
|
||||
"wled00/data",
|
||||
@@ -394,12 +330,6 @@ writeChunks(
|
||||
name: "PAGE_settings_pin",
|
||||
method: "gzip",
|
||||
filter: "html-minify"
|
||||
},
|
||||
{
|
||||
file: "settings_pininfo.htm",
|
||||
name: "PAGE_settings_pininfo",
|
||||
method: "gzip",
|
||||
filter: "html-minify"
|
||||
}
|
||||
],
|
||||
"wled00/html_settings.h"
|
||||
@@ -445,6 +375,12 @@ const char PAGE_dmxmap[] PROGMEM = R"=====()=====";
|
||||
name: "PAGE_update",
|
||||
method: "gzip",
|
||||
filter: "html-minify",
|
||||
mangle: (str) =>
|
||||
str
|
||||
.replace(
|
||||
/function GetV().*\<\/script\>/gms,
|
||||
"</script><script src=\"/settings/s.js?p=9\"></script>"
|
||||
)
|
||||
},
|
||||
{
|
||||
file: "welcome.htm",
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
# ESP-IDF Partition Table
|
||||
# Name, Type, SubType, Offset, Size, Flags
|
||||
# bootloader.bin,, 0x1000, 32K
|
||||
# partition table,, 0x8000, 4K
|
||||
nvs, data, nvs, 0x9000, 20K,
|
||||
otadata, data, ota, 0xe000, 8K,
|
||||
ota_0, app, ota_0, 0x10000, 2048K,
|
||||
ota_1, app, ota_1, 0x210000, 2048K,
|
||||
uf2, app, factory,0x410000, 256K,
|
||||
spiffs, data, spiffs, 0x450000, 11968K,
|
||||
|
@@ -1,11 +0,0 @@
|
||||
# ESP-IDF Partition Table
|
||||
# Name, Type, SubType, Offset, Size, Flags
|
||||
# bootloader.bin,, 0x1000, 32K
|
||||
# partition table, 0x8000, 4K
|
||||
|
||||
nvs, data, nvs, 0x9000, 20K,
|
||||
otadata, data, ota, 0xe000, 8K,
|
||||
ota_0, 0, ota_0, 0x10000, 1408K,
|
||||
ota_1, 0, ota_1, 0x170000, 1408K,
|
||||
uf2, app, factory,0x2d0000, 256K,
|
||||
spiffs, data, spiffs, 0x310000, 960K,
|
||||
|
@@ -1,10 +0,0 @@
|
||||
# ESP-IDF Partition Table
|
||||
# Name, Type, SubType, Offset, Size, Flags
|
||||
# bootloader.bin,, 0x1000, 32K
|
||||
# partition table,, 0x8000, 4K
|
||||
nvs, data, nvs, 0x9000, 20K,
|
||||
otadata, data, ota, 0xe000, 8K,
|
||||
ota_0, app, ota_0, 0x10000, 2048K,
|
||||
ota_1, app, ota_1, 0x210000, 2048K,
|
||||
uf2, app, factory,0x410000, 256K,
|
||||
spiffs, data, spiffs, 0x450000, 3776K,
|
||||
|
@@ -1,696 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>WBF ↔ C Header Bi-Directional Converter</title>
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
}
|
||||
body {
|
||||
font-family: system-ui, sans-serif;
|
||||
background: #0a0a0a;
|
||||
color: #e0e0e0;
|
||||
padding: 20px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.card {
|
||||
max-width: 900px;
|
||||
margin: auto;
|
||||
background: #16213e;
|
||||
padding: 30px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 20px rgba(0, 242, 255, .15);
|
||||
border: 1px solid rgba(0, 242, 255, .1);
|
||||
}
|
||||
h1 {
|
||||
color: #00f2ff;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
font-size: 2rem;
|
||||
text-shadow: 0 0 25px #ffffff;
|
||||
}
|
||||
.mode-selector {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.mode-btn {
|
||||
padding: 14px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
border: 2px solid rgba(0, 242, 255, .3);
|
||||
background: rgba(0, 0, 0, .4);
|
||||
color: #888;
|
||||
transition: all .3s;
|
||||
}
|
||||
.mode-btn.active {
|
||||
background: linear-gradient(135deg, #0095b3 0%, #00d4ff 100%);
|
||||
border-color: #00f2ff;
|
||||
color: #fff;
|
||||
box-shadow: 0 0 14px rgba(0, 242, 255, .6);
|
||||
}
|
||||
.mode-btn:hover:not(.active) {
|
||||
border-color: #00f2ff;
|
||||
color: #00f2ff;
|
||||
}
|
||||
.controls {
|
||||
background: rgba(255, 255, 255, .03);
|
||||
padding: 20px;
|
||||
border-radius: 15px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid rgba(0, 242, 255, .1);
|
||||
display: none;
|
||||
}
|
||||
.controls.active {
|
||||
display: block;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
font-size: .85rem;
|
||||
font-weight: 600;
|
||||
color: #00d4ff;
|
||||
margin-bottom: 8px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .5px;
|
||||
}
|
||||
input[type="file"] {
|
||||
display: none;
|
||||
}
|
||||
.file-label {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(0, 242, 255, .2);
|
||||
background: rgba(0, 0, 0, .4);
|
||||
color: #888;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
transition: all .3s;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.file-label:hover {
|
||||
border-color: #00f2ff;
|
||||
color: #00f2ff;
|
||||
}
|
||||
.file-label.has-file {
|
||||
color: #00f2ff;
|
||||
}
|
||||
input[type="text"], textarea {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(0, 242, 255, .2);
|
||||
background: rgba(0, 0, 0, .4);
|
||||
color: #fff;
|
||||
margin-bottom: 15px;
|
||||
transition: all .3s;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
textarea {
|
||||
min-height: 300px;
|
||||
resize: vertical;
|
||||
font-size: .85rem;
|
||||
}
|
||||
input[type="text"]:focus, textarea:focus {
|
||||
outline: none;
|
||||
border-color: #00f2ff;
|
||||
box-shadow: 0 0 8px rgba(0, 242, 255, .3);
|
||||
}
|
||||
button {
|
||||
width: 100%;
|
||||
padding: 14px;
|
||||
border-radius: 50px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
border: none;
|
||||
background: linear-gradient(135deg, #0095b3 0%, #00d4ff 100%);
|
||||
box-shadow: 0 0 14px rgba(0, 242, 255, .6);
|
||||
color: #fff;
|
||||
transition: all .3s;
|
||||
}
|
||||
button:hover:not(:disabled) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 0 18px rgba(0, 242, 255, .9);
|
||||
}
|
||||
button:disabled {
|
||||
opacity: .3;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.info {
|
||||
background: rgba(0, 242, 255, .05);
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid rgba(0, 242, 255, .2);
|
||||
font-size: .85rem;
|
||||
}
|
||||
.output {
|
||||
background: rgba(0, 0, 0, .6);
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(0, 242, 255, .2);
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
display: none;
|
||||
}
|
||||
.output.active {
|
||||
display: block;
|
||||
}
|
||||
pre {
|
||||
color: #00f2ff;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: .85rem;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.copy-btn {
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<h1>WBF ↔ C Header Converter</h1>
|
||||
|
||||
<div class="mode-selector">
|
||||
<button class="mode-btn active" onclick="switchMode('wbf-to-header')">WBF → C Header</button>
|
||||
<button class="mode-btn" onclick="switchMode('header-to-wbf')">C Header → WBF</button>
|
||||
</div>
|
||||
|
||||
<!-- WBF to Header Mode -->
|
||||
<div id="wbf-to-header" class="controls active">
|
||||
<div class="info">
|
||||
Load a WLED Bitmap Font (.wbf) file and convert it to a C/C++ header file.
|
||||
</div>
|
||||
|
||||
<label>Select WBF Font File</label>
|
||||
<label for="wbfFile" class="file-label" id="wbfFileLabel">Choose .wbf file</label>
|
||||
<input type="file" id="wbfFile" accept=".wbf">
|
||||
|
||||
<label>Array Name</label>
|
||||
<input type="text" id="arrayName" placeholder="console_font_4x6" value="console_font_4x6">
|
||||
|
||||
<button id="convertToHeaderBtn" disabled>Convert to Header</button>
|
||||
</div>
|
||||
|
||||
<!-- Header to WBF Mode -->
|
||||
<div id="header-to-wbf" class="controls">
|
||||
<div class="info">
|
||||
Paste a C/C++ header file containing a WLED font array and convert it to a .wbf file.
|
||||
</div>
|
||||
|
||||
<label>Paste C Header Code</label>
|
||||
<textarea id="headerInput" placeholder="Paste your C header code here (e.g., static const unsigned char font[] PROGMEM = {...};)"></textarea>
|
||||
|
||||
<label>Output Filename (without .wbf extension)</label>
|
||||
<input type="text" id="wbfFilename" placeholder="console_font_4x6" value="console_font_4x6">
|
||||
|
||||
<button id="convertToWbfBtn" disabled>Convert to WBF</button>
|
||||
</div>
|
||||
|
||||
<div id="output" class="output">
|
||||
<pre id="outputCode"></pre>
|
||||
<button class="copy-btn" onclick="copyToClipboard(event)">Copy to Clipboard</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Elements for WBF to Header
|
||||
const wbfFileInput = document.getElementById('wbfFile');
|
||||
const wbfFileLabel = document.getElementById('wbfFileLabel');
|
||||
const arrayNameInput = document.getElementById('arrayName');
|
||||
const convertToHeaderBtn = document.getElementById('convertToHeaderBtn');
|
||||
|
||||
// Elements for Header to WBF
|
||||
const headerInput = document.getElementById('headerInput');
|
||||
const wbfFilenameInput = document.getElementById('wbfFilename');
|
||||
const convertToWbfBtn = document.getElementById('convertToWbfBtn');
|
||||
|
||||
// Output elements
|
||||
const output = document.getElementById('output');
|
||||
const outputCode = document.getElementById('outputCode');
|
||||
|
||||
let wbfData = null;
|
||||
let fileName = '';
|
||||
|
||||
// Mode switching
|
||||
function switchMode(mode) {
|
||||
document.querySelectorAll('.mode-btn').forEach(btn => btn.classList.remove('active'));
|
||||
document.querySelectorAll('.controls').forEach(ctrl => ctrl.classList.remove('active'));
|
||||
|
||||
event.target.classList.add('active');
|
||||
document.getElementById(mode).classList.add('active');
|
||||
output.classList.remove('active');
|
||||
}
|
||||
|
||||
// ==================== WBF to Header ====================
|
||||
wbfFileInput.addEventListener('change', (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
fileName = file.name.replace(/\.[^.]+$/, '');
|
||||
wbfFileLabel.textContent = file.name;
|
||||
wbfFileLabel.classList.add('has-file');
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (ev) => {
|
||||
wbfData = new Uint8Array(ev.target.result);
|
||||
convertToHeaderBtn.disabled = false;
|
||||
};
|
||||
reader.readAsArrayBuffer(file);
|
||||
}
|
||||
});
|
||||
|
||||
convertToHeaderBtn.addEventListener('click', () => {
|
||||
if (!wbfData) return;
|
||||
|
||||
const header = parseWBF(wbfData);
|
||||
if (header) {
|
||||
generateHeader(header);
|
||||
}
|
||||
});
|
||||
|
||||
function parseWBF(data) {
|
||||
if (data.length < 12) {
|
||||
alert('Invalid WBF file: too short');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Parse header
|
||||
if (data[0] !== 0x57) { // 'W'
|
||||
alert('Invalid WBF file: missing magic byte');
|
||||
return null;
|
||||
}
|
||||
|
||||
const height = data[1];
|
||||
const maxWidth = data[2];
|
||||
const spacing = data[3];
|
||||
const flags = data[4];
|
||||
const first = data[5];
|
||||
const last = data[6];
|
||||
const reserved = data[7];
|
||||
|
||||
// Unicode offset is 32-bit little-endian
|
||||
const unicodeOffset = data[8] | (data[9] << 8) | (data[10] << 16) | (data[11] << 24);
|
||||
|
||||
const numChars = last - first + 1;
|
||||
const isVariableWidth = (flags & 0x01) !== 0;
|
||||
|
||||
let offset = 12; // Start after header
|
||||
let widthTable = null;
|
||||
|
||||
// If variable width, read width table
|
||||
if (isVariableWidth) {
|
||||
if (data.length < 12 + numChars) {
|
||||
alert('Invalid WBF file: missing width table');
|
||||
return null;
|
||||
}
|
||||
widthTable = Array.from(data.slice(12, 12 + numChars));
|
||||
offset = 12 + numChars;
|
||||
}
|
||||
|
||||
// Calculate expected data size
|
||||
let expectedDataSize = 0;
|
||||
if (isVariableWidth) {
|
||||
for (let w of widthTable) {
|
||||
expectedDataSize += Math.ceil((w * height) / 8);
|
||||
}
|
||||
} else {
|
||||
expectedDataSize = numChars * Math.ceil((maxWidth * height) / 8);
|
||||
}
|
||||
|
||||
if (data.length < offset + expectedDataSize) {
|
||||
alert(`Invalid WBF file: expected ${offset + expectedDataSize} bytes, got ${data.length}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
height,
|
||||
maxWidth,
|
||||
spacing,
|
||||
flags,
|
||||
first,
|
||||
last,
|
||||
reserved,
|
||||
unicodeOffset,
|
||||
data: data,
|
||||
isVariableWidth,
|
||||
widthTable,
|
||||
dataOffset: offset
|
||||
};
|
||||
}
|
||||
|
||||
function generateHeader(header) {
|
||||
const arrayName = arrayNameInput.value || 'console_font';
|
||||
let code = '';
|
||||
|
||||
// Header comment
|
||||
code += `// Font: ${fileName}\n`;
|
||||
code += `// Height: ${header.height}, Max Width: ${header.maxWidth}, Spacing: ${header.spacing}\n`;
|
||||
code += `// Characters: ${header.first}-${header.last} (${header.last - header.first + 1} glyphs)\n`;
|
||||
code += `// Unicode Offset: 0x${header.unicodeOffset.toString(16).padStart(8, '0').toUpperCase()}\n`;
|
||||
code += `// Variable Width: ${header.isVariableWidth ? 'Yes' : 'No'}\n\n`;
|
||||
|
||||
// Array declaration
|
||||
code += `static const unsigned char ${arrayName}[] PROGMEM = {\n`;
|
||||
|
||||
// Header bytes (12 bytes)
|
||||
code += ' ';
|
||||
code += `0x${header.data[0].toString(16).padStart(2, '0').toUpperCase()}, `; // Magic 'W'
|
||||
code += `0x${header.height.toString(16).padStart(2, '0').toUpperCase()}, `; // Height
|
||||
code += `0x${header.maxWidth.toString(16).padStart(2, '0').toUpperCase()}, `; // Max Width
|
||||
code += `0x${header.spacing.toString(16).padStart(2, '0').toUpperCase()}, `; // Spacing
|
||||
code += `0x${header.flags.toString(16).padStart(2, '0').toUpperCase()}, `; // Flags
|
||||
code += `0x${header.first.toString(16).padStart(2, '0').toUpperCase()}, `; // First char
|
||||
code += `0x${header.last.toString(16).padStart(2, '0').toUpperCase()}, `; // Last char
|
||||
code += `0x${header.reserved.toString(16).padStart(2, '0').toUpperCase()}, `; // Reserved
|
||||
|
||||
// Unicode offset (4 bytes, little-endian)
|
||||
code += `0x${header.data[8].toString(16).padStart(2, '0').toUpperCase()}, `;
|
||||
code += `0x${header.data[9].toString(16).padStart(2, '0').toUpperCase()}, `;
|
||||
code += `0x${header.data[10].toString(16).padStart(2, '0').toUpperCase()}, `;
|
||||
code += `0x${header.data[11].toString(16).padStart(2, '0').toUpperCase()}`;
|
||||
|
||||
code += ', // Header: \'W\', H, W, S, Flags, First, Last, Reserved, UnicodeOffset (32bit)\n\n';
|
||||
|
||||
// Width table (if variable width)
|
||||
if (header.isVariableWidth) {
|
||||
code += ' // Width table\n';
|
||||
const numChars = header.last - header.first + 1;
|
||||
for (let i = 0; i < numChars; i++) {
|
||||
if (i % 16 === 0) {
|
||||
if (i > 0) code += '\n';
|
||||
code += ' ';
|
||||
}
|
||||
code += `0x${header.widthTable[i].toString(16).padStart(2, '0').toUpperCase()}`;
|
||||
if (i < numChars - 1) {
|
||||
code += ', ';
|
||||
}
|
||||
}
|
||||
code += ',\n\n';
|
||||
}
|
||||
|
||||
// Character data
|
||||
const numChars = header.last - header.first + 1;
|
||||
let offset = header.dataOffset;
|
||||
|
||||
// First pass: calculate max byte width for alignment
|
||||
let maxByteWidth = 0;
|
||||
let tempOffset = header.dataOffset;
|
||||
for (let i = 0; i < numChars; i++) {
|
||||
const charWidth = header.isVariableWidth ? header.widthTable[i] : header.maxWidth;
|
||||
const bytesPerChar = Math.ceil((charWidth * header.height) / 8);
|
||||
const byteStr = Array(bytesPerChar).fill('0x00').join(', ');
|
||||
maxByteWidth = Math.max(maxByteWidth, byteStr.length);
|
||||
tempOffset += bytesPerChar;
|
||||
}
|
||||
|
||||
// Second pass: generate output with aligned comments
|
||||
for (let i = 0; i < numChars; i++) {
|
||||
const charCode = header.first + i;
|
||||
const charStr = getAsciiString(charCode);
|
||||
|
||||
// Calculate bytes for this character
|
||||
const charWidth = header.isVariableWidth ? header.widthTable[i] : header.maxWidth;
|
||||
const bytesPerChar = Math.ceil((charWidth * header.height) / 8);
|
||||
|
||||
code += ' ';
|
||||
let byteStr = '';
|
||||
for (let b = 0; b < bytesPerChar; b++) {
|
||||
const byte = header.data[offset++];
|
||||
byteStr += `0x${byte.toString(16).padStart(2, '0').toUpperCase()}`;
|
||||
if (b < bytesPerChar - 1) {
|
||||
byteStr += ', ';
|
||||
}
|
||||
}
|
||||
code += byteStr;
|
||||
code += ',';
|
||||
|
||||
// Pad to align comments
|
||||
const padding = ' '.repeat(maxByteWidth - byteStr.length + 5);
|
||||
|
||||
const widthInfo = header.isVariableWidth ? `, w=${charWidth}` : '';
|
||||
code += `${padding}/* code=${charCode}, hex=0x${charCode.toString(16).padStart(2, '0').toUpperCase()}, ascii="${charStr}"${widthInfo} */\n`;
|
||||
}
|
||||
|
||||
code += '};\n';
|
||||
|
||||
// Display output
|
||||
outputCode.textContent = code;
|
||||
output.classList.add('active');
|
||||
}
|
||||
|
||||
// ==================== Header to WBF ====================
|
||||
headerInput.addEventListener('input', () => {
|
||||
convertToWbfBtn.disabled = headerInput.value.trim().length === 0;
|
||||
});
|
||||
|
||||
convertToWbfBtn.addEventListener('click', () => {
|
||||
const headerText = headerInput.value.trim();
|
||||
if (!headerText) return;
|
||||
|
||||
const wbfData = parseHeaderToWBF(headerText);
|
||||
if (wbfData) {
|
||||
downloadWBF(wbfData);
|
||||
}
|
||||
});
|
||||
|
||||
function parseHeaderToWBF(headerText) {
|
||||
try {
|
||||
// Properly remove C-style comments
|
||||
let cleanedText = removeComments(headerText);
|
||||
|
||||
// Extract all hex values from the cleaned text
|
||||
const hexPattern = /0x[0-9a-fA-F]{2}/g;
|
||||
const hexValues = cleanedText.match(hexPattern);
|
||||
|
||||
if (!hexValues || hexValues.length < 12) {
|
||||
alert('Invalid header: Could not find enough hex values (need at least 12 for header)\nFound: ' + (hexValues ? hexValues.length : 0) + ' bytes');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Convert hex strings to bytes
|
||||
const allBytes = hexValues.map(hex => parseInt(hex, 16));
|
||||
|
||||
// Parse the header (first 12 bytes)
|
||||
const height = allBytes[1];
|
||||
const maxWidth = allBytes[2];
|
||||
const spacing = allBytes[3];
|
||||
const flags = allBytes[4];
|
||||
const first = allBytes[5];
|
||||
const last = allBytes[6];
|
||||
const reserved = allBytes[7];
|
||||
const unicodeOffset = allBytes[8] | (allBytes[9] << 8) | (allBytes[10] << 16) | (allBytes[11] << 24);
|
||||
|
||||
// Validate magic byte
|
||||
if (allBytes[0] !== 0x57) {
|
||||
alert('Invalid header: First byte should be 0x57 (magic \'W\'), found 0x' + allBytes[0].toString(16).padStart(2, '0').toUpperCase());
|
||||
return null;
|
||||
}
|
||||
|
||||
const numChars = last - first + 1;
|
||||
const isVariableWidth = (flags & 0x01) !== 0;
|
||||
|
||||
// Now we need to separate header, width table (if present), and bitmap data
|
||||
let dataStartIndex = 12; // After the 12-byte header
|
||||
let widthTable = null;
|
||||
|
||||
if (isVariableWidth) {
|
||||
// Extract width table
|
||||
widthTable = allBytes.slice(dataStartIndex, dataStartIndex + numChars);
|
||||
dataStartIndex += numChars;
|
||||
}
|
||||
|
||||
// Calculate expected bitmap data size
|
||||
let expectedBitmapSize = 0;
|
||||
if (isVariableWidth) {
|
||||
for (let w of widthTable) {
|
||||
expectedBitmapSize += Math.ceil((w * height) / 8);
|
||||
}
|
||||
} else {
|
||||
expectedBitmapSize = numChars * Math.ceil((maxWidth * height) / 8);
|
||||
}
|
||||
|
||||
// Extract bitmap data
|
||||
const bitmapData = allBytes.slice(dataStartIndex, dataStartIndex + expectedBitmapSize);
|
||||
|
||||
// Validate we have all the data
|
||||
const totalExpected = dataStartIndex + expectedBitmapSize;
|
||||
if (allBytes.length < totalExpected) {
|
||||
alert(`Invalid header: Expected at least ${totalExpected} bytes, found ${allBytes.length}\n\n` +
|
||||
`Header shows: ${numChars} characters from ${first} to ${last}\n` +
|
||||
`${isVariableWidth ? 'Variable width font' : 'Fixed width font'}\n` +
|
||||
`Height: ${height}, Max Width: ${maxWidth}\n` +
|
||||
`Expected bitmap size: ${expectedBitmapSize} bytes`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Reconstruct the WBF file
|
||||
let wbfSize = 12; // Header
|
||||
if (isVariableWidth) {
|
||||
wbfSize += numChars; // Width table
|
||||
}
|
||||
wbfSize += expectedBitmapSize; // Bitmap data
|
||||
|
||||
const wbfData = new Uint8Array(wbfSize);
|
||||
let offset = 0;
|
||||
|
||||
// Write header
|
||||
wbfData[offset++] = 0x57; // Magic 'W'
|
||||
wbfData[offset++] = height;
|
||||
wbfData[offset++] = maxWidth;
|
||||
wbfData[offset++] = spacing;
|
||||
wbfData[offset++] = flags;
|
||||
wbfData[offset++] = first;
|
||||
wbfData[offset++] = last;
|
||||
wbfData[offset++] = reserved;
|
||||
wbfData[offset++] = unicodeOffset & 0xFF;
|
||||
wbfData[offset++] = (unicodeOffset >> 8) & 0xFF;
|
||||
wbfData[offset++] = (unicodeOffset >> 16) & 0xFF;
|
||||
wbfData[offset++] = (unicodeOffset >> 24) & 0xFF;
|
||||
|
||||
// Write width table if variable width
|
||||
if (isVariableWidth) {
|
||||
for (let w of widthTable) {
|
||||
wbfData[offset++] = w;
|
||||
}
|
||||
}
|
||||
|
||||
// Write bitmap data
|
||||
for (let byte of bitmapData) {
|
||||
wbfData[offset++] = byte;
|
||||
}
|
||||
|
||||
// Show success message
|
||||
outputCode.textContent = `Successfully parsed header!\n\n` +
|
||||
`Height: ${height}\n` +
|
||||
`Max Width: ${maxWidth}\n` +
|
||||
`Spacing: ${spacing}\n` +
|
||||
`Flags: 0x${flags.toString(16).padStart(2, '0').toUpperCase()} (${isVariableWidth ? 'Variable Width' : 'Fixed Width'})\n` +
|
||||
`Characters: ${first}-${last} (${numChars} glyphs)\n` +
|
||||
`Unicode Offset: 0x${unicodeOffset.toString(16).padStart(8, '0').toUpperCase()}\n` +
|
||||
`Total Size: ${wbfData.length} bytes\n` +
|
||||
` Header: 12 bytes\n` +
|
||||
(isVariableWidth ? ` Width table: ${numChars} bytes\n` : '') +
|
||||
` Bitmap data: ${expectedBitmapSize} bytes\n` +
|
||||
`Parsed ${allBytes.length} total hex values\n\n` +
|
||||
`Your .wbf file is ready to download!`;
|
||||
output.classList.add('active');
|
||||
|
||||
return wbfData;
|
||||
} catch (error) {
|
||||
alert('Error parsing header: ' + error.message);
|
||||
console.error('Parse error:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Proper C-style comment removal
|
||||
function removeComments(code) {
|
||||
let result = '';
|
||||
let i = 0;
|
||||
|
||||
while (i < code.length) {
|
||||
// Check for // comment (single-line)
|
||||
if (code[i] === '/' && code[i + 1] === '/') {
|
||||
// Skip until end of line
|
||||
i += 2;
|
||||
while (i < code.length && code[i] !== '\n') {
|
||||
i++;
|
||||
}
|
||||
// Keep the newline
|
||||
if (i < code.length) {
|
||||
result += '\n';
|
||||
i++;
|
||||
}
|
||||
}
|
||||
// Check for /* comment (multi-line)
|
||||
else if (code[i] === '/' && code[i + 1] === '*') {
|
||||
// Skip until we find */
|
||||
i += 2;
|
||||
while (i < code.length - 1) {
|
||||
if (code[i] === '*' && code[i + 1] === '/') {
|
||||
i += 2;
|
||||
break;
|
||||
}
|
||||
// Preserve newlines in multi-line comments (for line counting if needed)
|
||||
if (code[i] === '\n') {
|
||||
result += '\n';
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
// Regular character
|
||||
else {
|
||||
result += code[i];
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function downloadWBF(data) {
|
||||
const filename = wbfFilenameInput.value.trim() || 'font';
|
||||
const blob = new Blob([data], { type: 'application/octet-stream' });
|
||||
const a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = `${filename}.wbf`;
|
||||
a.click();
|
||||
}
|
||||
|
||||
// ==================== Utility Functions ====================
|
||||
function getAsciiString(code) {
|
||||
if (code < 32) {
|
||||
// Control characters
|
||||
return '^' + String.fromCharCode(64 + code);
|
||||
} else if (code === 127) {
|
||||
return '^?';
|
||||
} else if (code >= 32 && code <= 126) {
|
||||
// Printable ASCII
|
||||
return String.fromCharCode(code);
|
||||
} else {
|
||||
// Extended ASCII
|
||||
return '\\x' + code.toString(16).padStart(2, '0');
|
||||
}
|
||||
}
|
||||
|
||||
function copyToClipboard(event) {
|
||||
const text = outputCode.textContent;
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
const btn = event.target;
|
||||
const originalText = btn.textContent;
|
||||
btn.textContent = 'Copied!';
|
||||
setTimeout(() => {
|
||||
btn.textContent = originalText;
|
||||
}, 2000);
|
||||
}).catch(err => {
|
||||
console.error('Clipboard error:', err);
|
||||
// Fallback method for older browsers
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = text;
|
||||
textarea.style.position = 'fixed';
|
||||
textarea.style.opacity = '0';
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
const btn = event.target;
|
||||
const originalText = btn.textContent;
|
||||
btn.textContent = 'Copied!';
|
||||
setTimeout(() => {
|
||||
btn.textContent = originalText;
|
||||
}, 2000);
|
||||
} catch (e) {
|
||||
alert('Failed to copy to clipboard: ' + e.message);
|
||||
}
|
||||
document.body.removeChild(textarea);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+37
-88
@@ -28,78 +28,28 @@ log() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Fetch a URL to a destination file, validating status codes.
|
||||
# Usage: fetch "<url>" "<dest or empty>" "200 404"
|
||||
fetch() {
|
||||
local url="$1"
|
||||
local dest="$2"
|
||||
local accepted="${3:-200}"
|
||||
# Generic curl handler function
|
||||
curl_handler() {
|
||||
local command="$1"
|
||||
local hostname="$2"
|
||||
|
||||
# If no dest given, just discard body
|
||||
local out
|
||||
if [ -n "$dest" ]; then
|
||||
# Write to ".tmp" files first, then move when success, to ensure we don't write partial files
|
||||
out="${dest}.tmp"
|
||||
response=$($command -w "%{http_code}" -o /dev/null)
|
||||
curl_exit_code=$?
|
||||
|
||||
if [ "$response" -ge 200 ] && [ "$response" -lt 300 ]; then
|
||||
return 0
|
||||
elif [ $curl_exit_code -ne 0 ]; then
|
||||
log "ERROR" "$RED" "Connection error during request to $hostname (curl exit code: $curl_exit_code)."
|
||||
return 1
|
||||
elif [ "$response" -ge 400 ]; then
|
||||
log "ERROR" "$RED" "Server error during request to $hostname (HTTP status code: $response)."
|
||||
return 2
|
||||
else
|
||||
out="/dev/null"
|
||||
log "ERROR" "$RED" "Unexpected response from $hostname (HTTP status code: $response)."
|
||||
return 3
|
||||
fi
|
||||
|
||||
response=$(curl --connect-timeout 5 --max-time 30 -s -w "%{http_code}" -o "$out" "$url")
|
||||
local curl_exit_code=$?
|
||||
|
||||
if [ $curl_exit_code -ne 0 ]; then
|
||||
[ -n "$dest" ] && rm -f "$out"
|
||||
log "ERROR" "$RED" "Connection error during request to $url (curl exit code: $curl_exit_code)."
|
||||
return 1
|
||||
fi
|
||||
|
||||
for code in $accepted; do
|
||||
if [ "$response" = "$code" ]; then
|
||||
# Accepted; only persist body for 2xx responses
|
||||
if [ -n "$dest" ]; then
|
||||
if [[ "$response" =~ ^2 ]]; then
|
||||
mv "$out" "$dest"
|
||||
else
|
||||
rm -f "$out"
|
||||
fi
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
# not accepted
|
||||
[ -n "$dest" ] && rm -f "$out"
|
||||
log "ERROR" "$RED" "Unexpected response from $url (HTTP $response)."
|
||||
return 2
|
||||
}
|
||||
|
||||
|
||||
# POST a file to a URL, validating status codes.
|
||||
# Usage: post_file "<url>" "<file>" "200"
|
||||
post_file() {
|
||||
local url="$1"
|
||||
local file="$2"
|
||||
local accepted="${3:-200}"
|
||||
|
||||
response=$(curl --connect-timeout 5 --max-time 300 -s -w "%{http_code}" -o /dev/null -X POST -F "file=@$file" "$url")
|
||||
local curl_exit_code=$?
|
||||
|
||||
if [ $curl_exit_code -ne 0 ]; then
|
||||
log "ERROR" "$RED" "Connection error during POST to $url (curl exit code: $curl_exit_code)."
|
||||
return 1
|
||||
fi
|
||||
|
||||
for code in $accepted; do
|
||||
if [ "$response" -eq "$code" ]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
log "ERROR" "$RED" "Unexpected response from $url (HTTP $response)."
|
||||
return 2
|
||||
}
|
||||
|
||||
|
||||
# Print help message
|
||||
show_help() {
|
||||
cat << EOF
|
||||
@@ -141,16 +91,8 @@ discover_devices() {
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Map avahi responses to strings separated by 0x1F (unit separator), deduplicated
|
||||
mapfile -t raw_devices < <(
|
||||
avahi-browse _wled._tcp --terminate -r -p |
|
||||
awk -F';' '
|
||||
/^=/ {
|
||||
key = $7 "\x1F" $8 "\x1F" $9
|
||||
if (!seen[key]++) print key
|
||||
}
|
||||
'
|
||||
)
|
||||
# Map avahi responses to strings seperated by 0x1F (unit separator)
|
||||
mapfile -t raw_devices < <(avahi-browse _wled._tcp --terminate -r -p | awk -F';' '/^=/ {print $7"\x1F"$8"\x1F"$9}')
|
||||
|
||||
local devices_array=()
|
||||
for device in "${raw_devices[@]}"; do
|
||||
@@ -167,27 +109,33 @@ backup_one() {
|
||||
local address="$2"
|
||||
local port="$3"
|
||||
|
||||
log "INFO" "$YELLOW" "Backing up device config/presets/ir: $hostname ($address:$port)"
|
||||
log "INFO" "$YELLOW" "Backing up device config/presets: $hostname ($address:$port)"
|
||||
|
||||
mkdir -p "$backup_dir"
|
||||
|
||||
local file_prefix="${backup_dir}/${hostname}"
|
||||
local cfg_url="http://$address:$port/cfg.json"
|
||||
local presets_url="http://$address:$port/presets.json"
|
||||
local cfg_dest="${backup_dir}/${hostname}.cfg.json"
|
||||
local presets_dest="${backup_dir}/${hostname}.presets.json"
|
||||
|
||||
if ! fetch "http://$address:$port/cfg.json" "${file_prefix}.cfg.json"; then
|
||||
# Write to ".tmp" files first, then move when success, to ensure we don't write partial files
|
||||
local curl_command_cfg="curl -s "$cfg_url" -o "$cfg_dest.tmp""
|
||||
local curl_command_presets="curl -s "$presets_url" -o "$presets_dest.tmp""
|
||||
|
||||
if ! curl_handler "$curl_command_cfg" "$hostname"; then
|
||||
log "ERROR" "$RED" "Failed to backup configuration for $hostname"
|
||||
rm -f "$cfg_dest.tmp"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! fetch "http://$address:$port/presets.json" "${file_prefix}.presets.json"; then
|
||||
if ! curl_handler "$curl_command_presets" "$hostname"; then
|
||||
log "ERROR" "$RED" "Failed to backup presets for $hostname"
|
||||
rm -f "$presets_dest.tmp"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# ir.json is optional
|
||||
if ! fetch "http://$address:$port/ir.json" "${file_prefix}.ir.json" "200 404"; then
|
||||
log "ERROR" "$RED" "Failed to backup ir configs for $hostname"
|
||||
fi
|
||||
fi
|
||||
|
||||
mv "$cfg_dest.tmp" "$cfg_dest"
|
||||
mv "$presets_dest.tmp" "$presets_dest"
|
||||
log "INFO" "$GREEN" "Successfully backed up config and presets for $hostname"
|
||||
return 0
|
||||
}
|
||||
@@ -202,8 +150,9 @@ update_one() {
|
||||
log "INFO" "$YELLOW" "Starting firmware update for device: $hostname ($address:$port)"
|
||||
|
||||
local url="http://$address:$port/update"
|
||||
local curl_command="curl -s -X POST -F "file=@$firmware" "$url""
|
||||
|
||||
if ! post_file "$url" "$firmware" "200"; then
|
||||
if ! curl_handler "$curl_command" "$hostname"; then
|
||||
log "ERROR" "$RED" "Failed to update firmware for $hostname"
|
||||
return 1
|
||||
fi
|
||||
|
||||
@@ -28,19 +28,18 @@ class ADS1115Usermod : public Usermod {
|
||||
}
|
||||
|
||||
void loop() {
|
||||
if (!isEnabled || strip.isUpdating() || millis() - lastTime <= loopInterval)
|
||||
return;
|
||||
if (isEnabled && millis() - lastTime > loopInterval) {
|
||||
lastTime = millis();
|
||||
|
||||
lastTime = millis();
|
||||
// If we don't have new data, skip this iteration.
|
||||
if (!ads.conversionComplete()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If we don't have new data, skip this iteration.
|
||||
if (!ads.conversionComplete()) {
|
||||
return;
|
||||
updateResult();
|
||||
moveToNextChannel();
|
||||
startReading();
|
||||
}
|
||||
|
||||
updateResult();
|
||||
moveToNextChannel();
|
||||
startReading();
|
||||
}
|
||||
|
||||
void addToJsonInfo(JsonObject& root)
|
||||
@@ -70,8 +69,6 @@ class ADS1115Usermod : public Usermod {
|
||||
{
|
||||
JsonObject top = root.createNestedObject(F("ADC ADS1115"));
|
||||
|
||||
top[F("Loop Interval")] = loopInterval;
|
||||
|
||||
for (uint8_t i = 0; i < channelsCount; i++) {
|
||||
ChannelSettings* settingsPtr = &(channelSettings[i]);
|
||||
JsonObject channel = top.createNestedObject(settingsPtr->settingName);
|
||||
@@ -82,6 +79,8 @@ class ADS1115Usermod : public Usermod {
|
||||
channel[F("Offset")] = settingsPtr->offset;
|
||||
channel[F("Decimals")] = settingsPtr->decimals;
|
||||
}
|
||||
|
||||
top[F("Loop Interval")] = loopInterval;
|
||||
}
|
||||
|
||||
bool readFromConfig(JsonObject& root)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "ADS1115_v2",
|
||||
"build": { "libArchive": false },
|
||||
"dependencies": {
|
||||
"Adafruit BusIO": "https://github.com/adafruit/Adafruit_BusIO#1.17.4",
|
||||
"Adafruit ADS1X15": "https://github.com/adafruit/Adafruit_ADS1X15#2.6.2"
|
||||
"Adafruit BusIO": "https://github.com/adafruit/Adafruit_BusIO#1.13.2",
|
||||
"Adafruit ADS1X15": "https://github.com/adafruit/Adafruit_ADS1X15#2.4.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +1,10 @@
|
||||
# ADS1115 Usermod
|
||||
# ADS1115 16-Bit ADC with four inputs
|
||||
|
||||
Reads values from an ADS1115 16-bit ADC and exposes them in the `Info` tab.
|
||||
This usermod will read from an ADS1115 ADC. The voltages are displayed in the Info section of the web UI.
|
||||
|
||||
## Features
|
||||
- Reads values from an ADS1115 over I2C.
|
||||
- Supports 8 ADS1115 input modes:
|
||||
- 4 single-ended inputs (`AIN0` to `AIN3`)
|
||||
- 4 differential pairs (`AIN0-AIN1`, `AIN0-AIN3`, `AIN1-AIN3`, `AIN2-AIN3`)
|
||||
- Per-channel configuration in the Usermod settings:
|
||||
- Enable/disable
|
||||
- Display name
|
||||
- Units
|
||||
- Multiplier and offset
|
||||
- Decimal precision
|
||||
- Configurable measurement loop interval.
|
||||
- Publishes configured channel values to the `Info` tab.
|
||||
Configuration is performed via the Usermod menu. There are no parameters to set in code!
|
||||
|
||||
## Compatibility
|
||||
- Requires an ADS1115 module connected via I2C.
|
||||
- Works on targets with I2C support.
|
||||
- Default ADC gain is `1x` (input range `+/-4.096V`).
|
||||
## Installation
|
||||
|
||||
## Installation
|
||||
- Add `ADS1115` to `custom_usermods` in your `platformio.ini` (or `platformio_override.ini`).
|
||||
|
||||
## Author
|
||||
- Dima Zhemkov [@dima-zhemkov](https://github.com/dima-zhemkov)
|
||||
Add 'ADS1115' to `custom_usermods` in your platformio environment.
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
[env:aht10_example]
|
||||
extends = env:esp32dev
|
||||
build_flags =
|
||||
${common.build_flags} ${esp32.build_flags}
|
||||
; -D USERMOD_AHT10_DEBUG ; -- add a debug status to the info modal
|
||||
@@ -3,6 +3,7 @@
|
||||
/*
|
||||
* Usermod for analog clock
|
||||
*/
|
||||
extern Timezone* tz;
|
||||
|
||||
class AnalogClockUsermod : public Usermod {
|
||||
private:
|
||||
@@ -115,7 +116,7 @@ private:
|
||||
);
|
||||
}
|
||||
|
||||
static inline uint32_t scale32(uint32_t c, uint8_t scale) {
|
||||
static inline uint32_t scale32(uint32_t c, fract8 scale) {
|
||||
return RGBW32(
|
||||
scale8(R(c), scale),
|
||||
scale8(G(c), scale),
|
||||
|
||||
@@ -25,8 +25,6 @@ class Animated_Staircase : public Usermod {
|
||||
unsigned int topMaxDist = 50; // default maximum measured distance in cm, top
|
||||
unsigned int bottomMaxDist = 50; // default maximum measured distance in cm, bottom
|
||||
bool togglePower = false; // toggle power on/off with staircase on/off
|
||||
bool topAPinInvert = false; // invert output of top sensor
|
||||
bool bottomAPinInvert = false; // invert output of bottom sensor
|
||||
|
||||
/* runtime variables */
|
||||
bool initDone = false;
|
||||
@@ -93,8 +91,6 @@ class Animated_Staircase : public Usermod {
|
||||
static const char _topEchoCm[];
|
||||
static const char _bottomEchoCm[];
|
||||
static const char _togglePower[];
|
||||
static const char _topPIRorTrigger_pin_invert[];
|
||||
static const char _bottomPIRorTrigger_pin_invert[];
|
||||
|
||||
void publishMqtt(bool bottom, const char* state) {
|
||||
#ifndef WLED_DISABLE_MQTT
|
||||
@@ -160,12 +156,6 @@ class Animated_Staircase : public Usermod {
|
||||
return pulseIn(echoPin, HIGH, maxTimeUs) > 0;
|
||||
}
|
||||
|
||||
bool readPIRPin(int8_t pin, bool invert) {
|
||||
if (pin < 0) return false;
|
||||
bool v = digitalRead(pin);
|
||||
return invert ? !v : v;
|
||||
}
|
||||
|
||||
bool checkSensors() {
|
||||
bool sensorChanged = false;
|
||||
|
||||
@@ -174,15 +164,15 @@ class Animated_Staircase : public Usermod {
|
||||
|
||||
bottomSensorRead = bottomSensorWrite ||
|
||||
(!useUSSensorBottom ?
|
||||
(bottomPIRorTriggerPin<0 ? false : readPIRPin(bottomPIRorTriggerPin, bottomAPinInvert)) :
|
||||
(bottomPIRorTriggerPin<0 ? false : digitalRead(bottomPIRorTriggerPin)) :
|
||||
ultrasoundRead(bottomPIRorTriggerPin, bottomEchoPin, bottomMaxDist*59) // cm to us
|
||||
);
|
||||
topSensorRead = topSensorWrite ||
|
||||
(!useUSSensorTop ?
|
||||
(topPIRorTriggerPin<0 ? false : readPIRPin(topPIRorTriggerPin, topAPinInvert)) :
|
||||
(topPIRorTriggerPin<0 ? false : digitalRead(topPIRorTriggerPin)) :
|
||||
ultrasoundRead(topPIRorTriggerPin, topEchoPin, topMaxDist*59) // cm to us
|
||||
);
|
||||
|
||||
|
||||
if (bottomSensorRead != bottomSensorState) {
|
||||
bottomSensorState = bottomSensorRead; // change previous state
|
||||
sensorChanged = true;
|
||||
@@ -449,20 +439,18 @@ class Animated_Staircase : public Usermod {
|
||||
if (staircase.isNull()) {
|
||||
staircase = root.createNestedObject(FPSTR(_name));
|
||||
}
|
||||
staircase[FPSTR(_enabled)] = enabled;
|
||||
staircase[FPSTR(_segmentDelay)] = segment_delay_ms;
|
||||
staircase[FPSTR(_onTime)] = on_time_ms / 1000;
|
||||
staircase[FPSTR(_useTopUltrasoundSensor)] = useUSSensorTop;
|
||||
staircase[FPSTR(_topPIRorTrigger_pin)] = topPIRorTriggerPin;
|
||||
staircase[FPSTR(_topEcho_pin)] = useUSSensorTop ? topEchoPin : -1;
|
||||
staircase[FPSTR(_useBottomUltrasoundSensor)] = useUSSensorBottom;
|
||||
staircase[FPSTR(_bottomPIRorTrigger_pin)] = bottomPIRorTriggerPin;
|
||||
staircase[FPSTR(_bottomEcho_pin)] = useUSSensorBottom ? bottomEchoPin : -1;
|
||||
staircase[FPSTR(_topEchoCm)] = topMaxDist;
|
||||
staircase[FPSTR(_bottomEchoCm)] = bottomMaxDist;
|
||||
staircase[FPSTR(_togglePower)] = togglePower;
|
||||
staircase[FPSTR(_topPIRorTrigger_pin_invert)] = topAPinInvert;
|
||||
staircase[FPSTR(_bottomPIRorTrigger_pin_invert)] = bottomAPinInvert;
|
||||
staircase[FPSTR(_enabled)] = enabled;
|
||||
staircase[FPSTR(_segmentDelay)] = segment_delay_ms;
|
||||
staircase[FPSTR(_onTime)] = on_time_ms / 1000;
|
||||
staircase[FPSTR(_useTopUltrasoundSensor)] = useUSSensorTop;
|
||||
staircase[FPSTR(_topPIRorTrigger_pin)] = topPIRorTriggerPin;
|
||||
staircase[FPSTR(_topEcho_pin)] = useUSSensorTop ? topEchoPin : -1;
|
||||
staircase[FPSTR(_useBottomUltrasoundSensor)] = useUSSensorBottom;
|
||||
staircase[FPSTR(_bottomPIRorTrigger_pin)] = bottomPIRorTriggerPin;
|
||||
staircase[FPSTR(_bottomEcho_pin)] = useUSSensorBottom ? bottomEchoPin : -1;
|
||||
staircase[FPSTR(_topEchoCm)] = topMaxDist;
|
||||
staircase[FPSTR(_bottomEchoCm)] = bottomMaxDist;
|
||||
staircase[FPSTR(_togglePower)] = togglePower;
|
||||
DEBUG_PRINTLN(F("Staircase config saved."));
|
||||
}
|
||||
|
||||
@@ -474,13 +462,11 @@ class Animated_Staircase : public Usermod {
|
||||
bool readFromConfig(JsonObject& root) {
|
||||
bool oldUseUSSensorTop = useUSSensorTop;
|
||||
bool oldUseUSSensorBottom = useUSSensorBottom;
|
||||
bool oldTopAPinInvert = topAPinInvert;
|
||||
bool oldBottomAPinInvert = bottomAPinInvert;
|
||||
int8_t oldTopAPin = topPIRorTriggerPin;
|
||||
int8_t oldTopBPin = topEchoPin;
|
||||
int8_t oldBottomAPin = bottomPIRorTriggerPin;
|
||||
int8_t oldBottomBPin = bottomEchoPin;
|
||||
|
||||
|
||||
JsonObject top = root[FPSTR(_name)];
|
||||
if (top.isNull()) {
|
||||
DEBUG_PRINT(FPSTR(_name));
|
||||
@@ -499,12 +485,10 @@ class Animated_Staircase : public Usermod {
|
||||
useUSSensorTop = top[FPSTR(_useTopUltrasoundSensor)] | useUSSensorTop;
|
||||
topPIRorTriggerPin = top[FPSTR(_topPIRorTrigger_pin)] | topPIRorTriggerPin;
|
||||
topEchoPin = top[FPSTR(_topEcho_pin)] | topEchoPin;
|
||||
topAPinInvert = top[FPSTR(_topPIRorTrigger_pin_invert)] | topAPinInvert;
|
||||
|
||||
|
||||
useUSSensorBottom = top[FPSTR(_useBottomUltrasoundSensor)] | useUSSensorBottom;
|
||||
bottomPIRorTriggerPin = top[FPSTR(_bottomPIRorTrigger_pin)] | bottomPIRorTriggerPin;
|
||||
bottomEchoPin = top[FPSTR(_bottomEcho_pin)] | bottomEchoPin;
|
||||
bottomAPinInvert = top[FPSTR(_bottomPIRorTrigger_pin_invert)] | bottomAPinInvert;
|
||||
|
||||
topMaxDist = top[FPSTR(_topEchoCm)] | topMaxDist;
|
||||
topMaxDist = min(150,max(30,(int)topMaxDist)); // max distance ~1.5m (a lag of 9ms may be expected)
|
||||
@@ -570,15 +554,14 @@ const char Animated_Staircase::_segmentDelay[] PROGMEM = "segment-d
|
||||
const char Animated_Staircase::_onTime[] PROGMEM = "on-time-s";
|
||||
const char Animated_Staircase::_useTopUltrasoundSensor[] PROGMEM = "useTopUltrasoundSensor";
|
||||
const char Animated_Staircase::_topPIRorTrigger_pin[] PROGMEM = "topPIRorTrigger_pin";
|
||||
const char Animated_Staircase::_topPIRorTrigger_pin_invert[] PROGMEM = "topPIRorTrigger_pin_invert";
|
||||
const char Animated_Staircase::_topEcho_pin[] PROGMEM = "topEcho_pin";
|
||||
const char Animated_Staircase::_useBottomUltrasoundSensor[] PROGMEM = "useBottomUltrasoundSensor";
|
||||
const char Animated_Staircase::_bottomPIRorTrigger_pin[] PROGMEM = "bottomPIRorTrigger_pin";
|
||||
const char Animated_Staircase::_bottomPIRorTrigger_pin_invert[] PROGMEM = "bottomPIRorTrigger_pin_invert";
|
||||
const char Animated_Staircase::_bottomEcho_pin[] PROGMEM = "bottomEcho_pin";
|
||||
const char Animated_Staircase::_topEchoCm[] PROGMEM = "top-dist-cm";
|
||||
const char Animated_Staircase::_bottomEchoCm[] PROGMEM = "bottom-dist-cm";
|
||||
const char Animated_Staircase::_togglePower[] PROGMEM = "toggle-on-off";
|
||||
|
||||
|
||||
static Animated_Staircase animated_staircase;
|
||||
REGISTER_USERMOD(animated_staircase);
|
||||
@@ -1,5 +1,5 @@
|
||||
#include "wled.h"
|
||||
#if defined(USERMOD_DHT_MQTT) && defined(WLED_DISABLE_MQTT)
|
||||
#ifdef WLED_DISABLE_MQTT
|
||||
#error "This user mod requires MQTT to be enabled."
|
||||
#endif
|
||||
|
||||
@@ -246,4 +246,4 @@ class UsermodDHT : public Usermod {
|
||||
|
||||
|
||||
static UsermodDHT dht;
|
||||
REGISTER_USERMOD(dht);
|
||||
REGISTER_USERMOD(dht);
|
||||
@@ -8,13 +8,13 @@
|
||||
; USERMOD_DHT_MQTT - publish measurements to the MQTT broker
|
||||
; USERMOD_DHT_STATS - For debug, report delay stats
|
||||
|
||||
[env:esp8266_2m_usermod_dht_C]
|
||||
extends = env:esp8266_2m
|
||||
custom_usermods = ${env:esp8266_2m.custom_usermods} DHT
|
||||
build_flags = ${env:esp8266_2m.build_flags} -D USERMOD_DHT_CELSIUS
|
||||
[env:d1_mini_usermod_dht_C]
|
||||
extends = env:d1_mini
|
||||
custom_usermods = ${env:d1_mini.custom_usermods} DHT
|
||||
build_flags = ${env:d1_mini.build_flags} -D USERMOD_DHT_CELSIUS
|
||||
|
||||
[env:esp32dev_LEDPIN_16_usermod_dht_C]
|
||||
extends = env:esp32dev
|
||||
custom_usermods = ${env:esp32dev.custom_usermods} DHT
|
||||
build_flags = ${env:esp32dev.build_flags} -D LEDPIN=16 -D USERMOD_DHT_CELSIUS -D USERMOD_DHT_STATS
|
||||
[env:custom32_LEDPIN_16_usermod_dht_C]
|
||||
extends = env:custom32_LEDPIN_16
|
||||
custom_usermods = ${env:custom32_LEDPIN_16.custom_usermods} DHT
|
||||
build_flags = ${env:custom32_LEDPIN_16.build_flags} -D USERMOD_DHT_CELSIUS -D USERMOD_DHT_STATS
|
||||
|
||||
@@ -114,7 +114,7 @@ class MyExampleUsermod : public Usermod {
|
||||
void loop() override {
|
||||
// if usermod is disabled or called during strip updating just exit
|
||||
// NOTE: on very long strips strip.isUpdating() may always return true so update accordingly
|
||||
if (!enabled || (strip.isUpdating() && (millis() - lastTime < 200))) return; // adjust "200" (in millisecond) to your needs - prevents starvation with very long strips
|
||||
if (!enabled || strip.isUpdating()) return;
|
||||
|
||||
// do your magic here
|
||||
if (millis() - lastTime > 1000) {
|
||||
@@ -176,7 +176,7 @@ class MyExampleUsermod : public Usermod {
|
||||
JsonObject usermod = root[FPSTR(_name)];
|
||||
if (!usermod.isNull()) {
|
||||
// expect JSON usermod data in usermod name object: {"ExampleUsermod:{"user0":10}"}
|
||||
userVar0 = usermod["user0"] | userVar0; //if "user0" key exists in JSON, update, else keep old value (userVar0 is defined in wled.h)
|
||||
userVar0 = usermod["user0"] | userVar0; //if "user0" key exists in JSON, update, else keep old value
|
||||
}
|
||||
// you can as well check WLED state JSON keys
|
||||
//if (root["bri"] == 255) Serial.println(F("Don't burn down your garage!"));
|
||||
@@ -313,11 +313,11 @@ class MyExampleUsermod : public Usermod {
|
||||
yield();
|
||||
// ignore certain button types as they may have other consequences
|
||||
if (!enabled
|
||||
|| buttons[b].type == BTN_TYPE_NONE
|
||||
|| buttons[b].type == BTN_TYPE_RESERVED
|
||||
|| buttons[b].type == BTN_TYPE_PIR_SENSOR
|
||||
|| buttons[b].type == BTN_TYPE_ANALOG
|
||||
|| buttons[b].type == BTN_TYPE_ANALOG_INVERTED) {
|
||||
|| buttonType[b] == BTN_TYPE_NONE
|
||||
|| buttonType[b] == BTN_TYPE_RESERVED
|
||||
|| buttonType[b] == BTN_TYPE_PIR_SENSOR
|
||||
|| buttonType[b] == BTN_TYPE_ANALOG
|
||||
|| buttonType[b] == BTN_TYPE_ANALOG_INVERTED) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "EleksTube_IPS",
|
||||
"name:": "EleksTube_IPS",
|
||||
"build": { "libArchive": false },
|
||||
"dependencies": {
|
||||
"TFT_eSPI" : "2.5.33"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Fix_unreachable_netservices_v2",
|
||||
"build": { "libArchive": false },
|
||||
"platforms": ["espressif8266"]
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ public:
|
||||
}
|
||||
if (m_updateConfig)
|
||||
{
|
||||
serializeConfigToFS();
|
||||
serializeConfig();
|
||||
m_updateConfig = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,38 +1,14 @@
|
||||
#include "wled.h"
|
||||
#include <INA226_WE.h>
|
||||
|
||||
#ifndef INA226_ADDRESS
|
||||
#define INA226_ADDRESS 0x40 // Default I2C address for INA226
|
||||
#endif
|
||||
|
||||
#ifndef INA226_CHECK_INTERVAL_MS
|
||||
#define INA226_CHECK_INTERVAL_MS 60000 // Default check interval in milliseconds
|
||||
#endif
|
||||
|
||||
#define DEFAULT_CHECKINTERVAL INA226_CHECK_INTERVAL_MS
|
||||
#define DEFAULT_CHECKINTERVAL 60000
|
||||
#define DEFAULT_INASAMPLES 128
|
||||
#define DEFAULT_INASAMPLESENUM AVERAGE_128
|
||||
#define DEFAULT_INACONVERSIONTIME 1100
|
||||
#define DEFAULT_INACONVERSIONTIMEENUM CONV_TIME_1100
|
||||
|
||||
// Compile-time defaults for shunt resistor (micro-ohms), current range (mA), and current offset (mA)
|
||||
// These can be overridden via -D flags in platformio.ini / platformio_override.ini
|
||||
#ifndef INA226_SHUNT_MICRO_OHMS
|
||||
#define INA226_SHUNT_MICRO_OHMS 1000000 // 1 Ohm = 1,000,000 μΩ
|
||||
#endif
|
||||
|
||||
#ifndef INA226_DEFAULT_CURRENT_RANGE
|
||||
#define INA226_DEFAULT_CURRENT_RANGE 1000 // 1000 mA = 1 A
|
||||
#endif
|
||||
|
||||
#ifndef INA226_CURRENT_OFFSET_MA
|
||||
#define INA226_CURRENT_OFFSET_MA 0 // No offset by default
|
||||
#endif
|
||||
|
||||
#ifndef INA226_ENABLED_DEFAULT
|
||||
#define INA226_ENABLED_DEFAULT false
|
||||
#endif
|
||||
|
||||
// A packed version of all INA settings enums and their human friendly counterparts packed into a 32 bit structure
|
||||
// Some values are shifted and need to be preprocessed before usage
|
||||
struct InaSettingLookup
|
||||
@@ -105,11 +81,10 @@ private:
|
||||
uint16_t _settingInaSamples : 11; // Number of samples for averaging, max 1024
|
||||
|
||||
uint8_t _i2cAddress;
|
||||
uint32_t _checkIntervalMs; // milliseconds, user settings is in seconds
|
||||
float _decimalFactor; // a power of 10 factor. 1 would be no change, 10 is one decimal, 100 is two etc. User sees a power of 10 (0, 1, 2, ..)
|
||||
uint32_t _shuntResistorUOhm; // Shunt resistor value in micro-ohms (μΩ)
|
||||
uint16_t _currentRangeMa; // Expected maximum current in milliamps
|
||||
int16_t _currentOffsetMa; // Current offset in milliamps, subtracted from readings
|
||||
uint16_t _checkInterval; // milliseconds, user settings is in seconds
|
||||
float _decimalFactor; // a power of 10 factor. 1 would be no change, 10 is one decimal, 100 is two etc. User sees a power of 10 (0, 1, 2, ..)
|
||||
uint16_t _shuntResistor; // Shunt resistor value in milliohms
|
||||
uint16_t _currentRange; // Expected maximum current in milliamps
|
||||
|
||||
uint8_t _lastStatus = 0;
|
||||
float _lastCurrent = 0;
|
||||
@@ -118,7 +93,7 @@ private:
|
||||
float _lastShuntVoltage = 0;
|
||||
bool _lastOverflow = false;
|
||||
|
||||
#ifndef WLED_DISABLE_MQTT
|
||||
#ifndef WLED_MQTT_DISABLE
|
||||
float _lastCurrentSent = 0;
|
||||
float _lastVoltageSent = 0;
|
||||
float _lastPowerSent = 0;
|
||||
@@ -143,11 +118,9 @@ private:
|
||||
_ina226 = new INA226_WE(_i2cAddress);
|
||||
if (!_ina226->init())
|
||||
{
|
||||
DEBUG_PRINTLN(F("INA226: init failed!"));
|
||||
DEBUG_PRINTLN(F("INA226 initialization failed!"));
|
||||
return;
|
||||
}
|
||||
DEBUG_PRINTF_P(PSTR("INA226: addr=0x%02X shunt=%luμΩ range=%umA offset=%dmA\n"),
|
||||
_i2cAddress, _shuntResistorUOhm, _currentRangeMa, _currentOffsetMa);
|
||||
_ina226->setCorrectionFactor(1.0);
|
||||
|
||||
uint16_t tmpShort = _settingInaSamples;
|
||||
@@ -156,7 +129,7 @@ private:
|
||||
tmpShort = _settingInaConversionTimeUs << 2;
|
||||
_ina226->setConversionTime(getConversionTimeEnum(tmpShort));
|
||||
|
||||
if (_checkIntervalMs >= 20000)
|
||||
if (_checkInterval >= 20000)
|
||||
{
|
||||
_isTriggeredOperationMode = true;
|
||||
_ina226->setMeasureMode(TRIGGERED);
|
||||
@@ -167,11 +140,7 @@ private:
|
||||
_ina226->setMeasureMode(CONTINUOUS);
|
||||
}
|
||||
|
||||
_ina226->setResistorRange(static_cast<float>(_shuntResistorUOhm) / 1000000.0f, static_cast<float>(_currentRangeMa) / 1000.0f);
|
||||
|
||||
DEBUG_PRINTF_P(PSTR("INA226: mode=%s interval=%lums samples=%u convTime=%uμs\n"),
|
||||
_isTriggeredOperationMode ? "triggered" : "continuous",
|
||||
_checkIntervalMs, _settingInaSamples, _settingInaConversionTimeUs << 2);
|
||||
_ina226->setResistorRange(static_cast<float>(_shuntResistor) / 1000.0, static_cast<float>(_currentRange) / 1000.0);
|
||||
}
|
||||
|
||||
void fetchAndPushValues()
|
||||
@@ -181,19 +150,17 @@ private:
|
||||
if (_lastStatus != 0)
|
||||
return;
|
||||
|
||||
float current = truncateDecimals((_ina226->getCurrent_mA() - _currentOffsetMa) / 1000.0f);
|
||||
float current = truncateDecimals(_ina226->getCurrent_mA() / 1000.0);
|
||||
float voltage = truncateDecimals(_ina226->getBusVoltage_V());
|
||||
float power = truncateDecimals(_ina226->getBusPower() / 1000.0f);
|
||||
float shuntVoltage = truncateDecimals(_ina226->getShuntVoltage_mV());
|
||||
float power = truncateDecimals(_ina226->getBusPower() / 1000.0);
|
||||
float shuntVoltage = truncateDecimals(_ina226->getShuntVoltage_V());
|
||||
bool overflow = _ina226->overflow;
|
||||
|
||||
#ifndef WLED_DISABLE_MQTT
|
||||
mqttPublishIfChanged(F("current"), _lastCurrentSent, current, 0.01f);
|
||||
mqttPublishIfChanged(F("voltage"), _lastVoltageSent, voltage, 0.01f);
|
||||
mqttPublishIfChanged(F("power"), _lastPowerSent, power, 0.1f);
|
||||
// Publish in V for backward compatibility
|
||||
float shuntVoltageV = shuntVoltage / 1000.0f;
|
||||
mqttPublishIfChanged(F("shunt_voltage"), _lastShuntVoltageSent, shuntVoltageV, 0.01f);
|
||||
mqttPublishIfChanged(F("shunt_voltage"), _lastShuntVoltageSent, shuntVoltage, 0.01f);
|
||||
mqttPublishIfChanged(F("overflow"), _lastOverflowSent, overflow);
|
||||
#endif
|
||||
|
||||
@@ -202,9 +169,6 @@ private:
|
||||
_lastPower = power;
|
||||
_lastShuntVoltage = shuntVoltage;
|
||||
_lastOverflow = overflow;
|
||||
|
||||
DEBUG_PRINTF_P(PSTR("INA226: %.3fA %.2fV %.2fW shunt=%.2fmV%s\n"),
|
||||
current, voltage, power, shuntVoltage, overflow ? " OVF" : "");
|
||||
}
|
||||
|
||||
void handleTriggeredMode(unsigned long currentTime)
|
||||
@@ -224,7 +188,7 @@ private:
|
||||
}
|
||||
else
|
||||
{
|
||||
if (currentTime - _lastLoopCheck >= _checkIntervalMs)
|
||||
if (currentTime - _lastLoopCheck >= _checkInterval)
|
||||
{
|
||||
// Start a measurement and use isBusy() later to determine when it is done
|
||||
_ina226->startSingleMeasurementNoWait();
|
||||
@@ -237,7 +201,7 @@ private:
|
||||
|
||||
void handleContinuousMode(unsigned long currentTime)
|
||||
{
|
||||
if (currentTime - _lastLoopCheck >= _checkIntervalMs)
|
||||
if (currentTime - _lastLoopCheck >= _checkInterval)
|
||||
{
|
||||
_lastLoopCheck = currentTime;
|
||||
fetchAndPushValues();
|
||||
@@ -251,23 +215,19 @@ private:
|
||||
return;
|
||||
|
||||
char topic[128];
|
||||
auto buildTopic = [&](const char *suffix) {
|
||||
snprintf_P(topic, sizeof(topic), PSTR("%s/%s"), mqttDeviceTopic, suffix);
|
||||
};
|
||||
|
||||
buildTopic("current");
|
||||
snprintf_P(topic, 127, "%s/current", mqttDeviceTopic);
|
||||
mqttCreateHassSensor(F("Current"), topic, F("current"), F("A"));
|
||||
|
||||
buildTopic("voltage");
|
||||
snprintf_P(topic, 127, "%s/voltage", mqttDeviceTopic);
|
||||
mqttCreateHassSensor(F("Voltage"), topic, F("voltage"), F("V"));
|
||||
|
||||
buildTopic("power");
|
||||
snprintf_P(topic, 127, "%s/power", mqttDeviceTopic);
|
||||
mqttCreateHassSensor(F("Power"), topic, F("power"), F("W"));
|
||||
|
||||
buildTopic("shunt_voltage");
|
||||
snprintf_P(topic, 127, "%s/shunt_voltage", mqttDeviceTopic);
|
||||
mqttCreateHassSensor(F("Shunt Voltage"), topic, F("voltage"), F("V"));
|
||||
|
||||
buildTopic("overflow");
|
||||
snprintf_P(topic, 127, "%s/overflow", mqttDeviceTopic);
|
||||
mqttCreateHassBinarySensor(F("Overflow"), topic);
|
||||
}
|
||||
|
||||
@@ -355,23 +315,14 @@ public:
|
||||
UsermodINA226()
|
||||
{
|
||||
// Default values
|
||||
_settingEnabled = INA226_ENABLED_DEFAULT;
|
||||
_settingInaSamples = DEFAULT_INASAMPLES;
|
||||
_settingInaConversionTimeUs = DEFAULT_INACONVERSIONTIME >> 2; // stored shifted to fit 12-bit field
|
||||
_settingInaConversionTimeUs = DEFAULT_INACONVERSIONTIME;
|
||||
|
||||
_i2cAddress = INA226_ADDRESS;
|
||||
_checkIntervalMs = DEFAULT_CHECKINTERVAL;
|
||||
_checkInterval = DEFAULT_CHECKINTERVAL;
|
||||
_decimalFactor = 100;
|
||||
_shuntResistorUOhm = INA226_SHUNT_MICRO_OHMS;
|
||||
_currentRangeMa = INA226_DEFAULT_CURRENT_RANGE;
|
||||
_currentOffsetMa = INA226_CURRENT_OFFSET_MA;
|
||||
|
||||
_mqttPublish = false;
|
||||
_mqttPublishAlways = false;
|
||||
_mqttHomeAssistant = false;
|
||||
_initDone = false;
|
||||
_isTriggeredOperationMode = false;
|
||||
_measurementTriggered = false;
|
||||
_shuntResistor = 1000;
|
||||
_currentRange = 1000;
|
||||
}
|
||||
|
||||
void setup()
|
||||
@@ -448,7 +399,7 @@ public:
|
||||
JsonArray jsonCurrent = user.createNestedArray(F("Current"));
|
||||
JsonArray jsonVoltage = user.createNestedArray(F("Voltage"));
|
||||
JsonArray jsonPower = user.createNestedArray(F("Power"));
|
||||
JsonArray jsonShuntVoltage = user.createNestedArray(F("Shunt Voltage Drop"));
|
||||
JsonArray jsonShuntVoltage = user.createNestedArray(F("Shunt Voltage"));
|
||||
JsonArray jsonOverflow = user.createNestedArray(F("Overflow"));
|
||||
|
||||
if (_lastLoopCheck == 0)
|
||||
@@ -481,7 +432,7 @@ public:
|
||||
jsonPower.add(F("W"));
|
||||
|
||||
jsonShuntVoltage.add(_lastShuntVoltage);
|
||||
jsonShuntVoltage.add(F("mV"));
|
||||
jsonShuntVoltage.add(F("V"));
|
||||
|
||||
jsonOverflow.add(_lastOverflow ? F("true") : F("false"));
|
||||
}
|
||||
@@ -491,13 +442,12 @@ public:
|
||||
JsonObject top = root.createNestedObject(FPSTR(_name));
|
||||
top[F("Enabled")] = _settingEnabled;
|
||||
top[F("I2CAddress")] = static_cast<uint8_t>(_i2cAddress);
|
||||
top[F("CheckInterval")] = _checkIntervalMs / 1000;
|
||||
top[F("CheckInterval")] = _checkInterval / 1000;
|
||||
top[F("INASamples")] = _settingInaSamples;
|
||||
top[F("INAConversionTime")] = _settingInaConversionTimeUs << 2;
|
||||
top[F("Decimals")] = log10f(_decimalFactor);
|
||||
top[F("ShuntResistor")] = static_cast<float>(_shuntResistorUOhm) / 1000.0f;
|
||||
top[F("CurrentRange")] = _currentRangeMa;
|
||||
top[F("CurrentOffset")] = _currentOffsetMa;
|
||||
top[F("ShuntResistor")] = _shuntResistor;
|
||||
top[F("CurrentRange")] = _currentRange;
|
||||
#ifndef WLED_DISABLE_MQTT
|
||||
top[F("MqttPublish")] = _mqttPublish;
|
||||
top[F("MqttPublishAlways")] = _mqttPublishAlways;
|
||||
@@ -507,17 +457,6 @@ public:
|
||||
DEBUG_PRINTLN(F("INA226 config saved."));
|
||||
}
|
||||
|
||||
void appendConfigData() override
|
||||
{
|
||||
oappend(F("addInfo('INA226:CheckInterval',1,'seconds');"));
|
||||
oappend(F("addInfo('INA226:INASamples',1,'samples (1-1024)');"));
|
||||
oappend(F("addInfo('INA226:INAConversionTime',1,'µs');"));
|
||||
oappend(F("addInfo('INA226:Decimals',1,'(0-5)');"));
|
||||
oappend(F("addInfo('INA226:ShuntResistor',1,'mΩ');"));
|
||||
oappend(F("addInfo('INA226:CurrentRange',1,'mA');"));
|
||||
oappend(F("addInfo('INA226:CurrentOffset',1,'mA');"));
|
||||
}
|
||||
|
||||
bool readFromConfig(JsonObject &root) override
|
||||
{
|
||||
JsonObject top = root[FPSTR(_name)];
|
||||
@@ -533,12 +472,12 @@ public:
|
||||
configComplete = false;
|
||||
|
||||
configComplete &= getJsonValue(top[F("I2CAddress")], _i2cAddress);
|
||||
if (getJsonValue(top[F("CheckInterval")], _checkIntervalMs))
|
||||
if (getJsonValue(top[F("CheckInterval")], _checkInterval))
|
||||
{
|
||||
if (1 <= _checkIntervalMs && _checkIntervalMs <= 600)
|
||||
_checkIntervalMs *= 1000;
|
||||
if (1 <= _checkInterval && _checkInterval <= 600)
|
||||
_checkInterval *= 1000;
|
||||
else
|
||||
_checkIntervalMs = DEFAULT_CHECKINTERVAL;
|
||||
_checkInterval = DEFAULT_CHECKINTERVAL;
|
||||
}
|
||||
else
|
||||
configComplete = false;
|
||||
@@ -572,26 +511,8 @@ public:
|
||||
else
|
||||
configComplete = false;
|
||||
|
||||
float shuntMilliOhms;
|
||||
if (getJsonValue(top[F("ShuntResistor")], shuntMilliOhms))
|
||||
{
|
||||
if (shuntMilliOhms > 0)
|
||||
_shuntResistorUOhm = static_cast<uint32_t>(shuntMilliOhms * 1000.0f + 0.5f);
|
||||
else
|
||||
_shuntResistorUOhm = INA226_SHUNT_MICRO_OHMS;
|
||||
}
|
||||
else
|
||||
configComplete = false;
|
||||
|
||||
if (getJsonValue(top[F("CurrentRange")], _currentRangeMa))
|
||||
{
|
||||
if (_currentRangeMa == 0 || _currentRangeMa > 20000)
|
||||
_currentRangeMa = INA226_DEFAULT_CURRENT_RANGE;
|
||||
}
|
||||
else
|
||||
configComplete = false;
|
||||
if (!getJsonValue(top[F("CurrentOffset")], _currentOffsetMa))
|
||||
_currentOffsetMa = INA226_CURRENT_OFFSET_MA; // Use compile-time default if missing from config
|
||||
configComplete &= getJsonValue(top[F("ShuntResistor")], _shuntResistor);
|
||||
configComplete &= getJsonValue(top[F("CurrentRange")], _currentRange);
|
||||
|
||||
#ifndef WLED_DISABLE_MQTT
|
||||
if (getJsonValue(top[F("MqttPublish")], tmpBool))
|
||||
|
||||
@@ -18,7 +18,6 @@ The following settings can be configured in the Usermod Menu:
|
||||
- **Decimals**: Number of decimals in the output.
|
||||
- **ShuntResistor**: Shunt resistor value in milliohms. An R100 shunt resistor should be written as "100", while R010 should be "10".
|
||||
- **CurrentRange**: Expected maximum current in milliamps (e.g., 5 A = 5000 mA).
|
||||
- **CurrentOffset**: Current offset in milliamps, subtracted from raw readings. Useful for compensating a consistent bias in the sensor. Default is 0.
|
||||
- **MqttPublish**: Enable or disable MQTT publishing.
|
||||
- **MqttPublishAlways**: Publish always, regardless if there is a change.
|
||||
- **MqttHomeAssistantDiscovery**: Enable Home Assistant discovery.
|
||||
@@ -64,33 +63,4 @@ extends = env:esp32dev
|
||||
custom_usermods = ${env:esp32dev.custom_usermods} INA226
|
||||
build_flags = ${env:esp32dev.build_flags}
|
||||
; -D USERMOD_INA226_DEBUG ; -- add a debug status to the info modal
|
||||
```
|
||||
|
||||
### Compile-time Defaults
|
||||
|
||||
Several parameters can be overridden at compile time via `-D` build flags. This is useful for setting board-specific defaults so the device works correctly on first boot without manual configuration.
|
||||
|
||||
| Build Flag | Default | Unit | Description |
|
||||
|---|---|---|---|
|
||||
| `INA226_ADDRESS` | `0x40` | — | I2C address of the INA226 |
|
||||
| `INA226_SHUNT_MICRO_OHMS` | `1000000` | μΩ | Shunt resistor value (1 000 000 μΩ = 1 Ω) |
|
||||
| `INA226_DEFAULT_CURRENT_RANGE` | `1000` | mA | Expected maximum current (1000 mA = 1 A) |
|
||||
| `INA226_CURRENT_OFFSET_MA` | `0` | mA | Current offset subtracted from readings |
|
||||
| `INA226_CHECK_INTERVAL_MS` | `60000` | ms | Default interval between readings on first boot |
|
||||
| `INA226_ENABLED_DEFAULT` | `false` | — | Enable the usermod on first boot |
|
||||
|
||||
Example for a board with a 2.888 mΩ effective shunt, 10 A range, -118 mA offset, 1 second polling, and enabled by default:
|
||||
|
||||
```ini
|
||||
[env:my_board]
|
||||
extends = env:esp32dev
|
||||
custom_usermods = ${env:esp32dev.custom_usermods} INA226
|
||||
build_flags = ${env:esp32dev.build_flags}
|
||||
-D INA226_ENABLED_DEFAULT=true
|
||||
-D INA226_SHUNT_MICRO_OHMS=2888
|
||||
-D INA226_DEFAULT_CURRENT_RANGE=10000
|
||||
-D INA226_CURRENT_OFFSET_MA=-118
|
||||
-D INA226_CHECK_INTERVAL_MS=1000
|
||||
```
|
||||
|
||||
All compile-time defaults can still be changed at runtime through the Usermod settings page.
|
||||
```
|
||||
@@ -0,0 +1,6 @@
|
||||
[env:ina226_example]
|
||||
extends = env:esp32dev
|
||||
custom_usermods = ${env:esp32dev.custom_usermods} INA226_v2
|
||||
build_flags =
|
||||
${env:esp32dev.build_flags}
|
||||
; -D USERMOD_INA226_DEBUG ; -- add a debug status to the info modal
|
||||
@@ -1,123 +0,0 @@
|
||||
#include "wled.h"
|
||||
#include "FXparticleSystem.h"
|
||||
|
||||
unsigned long nextCometCreationTime = 0;
|
||||
|
||||
#define FX_FALLBACK_STATIC { SEGMENT.fill(SEGCOLOR(0)); return; }
|
||||
// Use UINT32_MAX - 1 for the "no comet" case so we can add 1 later and not have it overflow
|
||||
#define NULL_INDEX UINT32_MAX - 1
|
||||
|
||||
///////////////////////
|
||||
// Effect Function //
|
||||
///////////////////////
|
||||
|
||||
void mode_pscomet() {
|
||||
ParticleSystem2D *PartSys = nullptr;
|
||||
uint32_t i;
|
||||
|
||||
if (SEGMENT.call == 0) { // Initialization
|
||||
// Try to allocate one comet for every column
|
||||
if (!initParticleSystem2D(PartSys, SEGMENT.vWidth())) {
|
||||
FX_FALLBACK_STATIC; // Allocation failed or not 2D
|
||||
}
|
||||
PartSys->setMotionBlur(170); // Enable motion blur
|
||||
PartSys->setParticleSize(0); // Allow small comets to be a single pixel wide
|
||||
}
|
||||
else {
|
||||
PartSys = reinterpret_cast<ParticleSystem2D *>(SEGENV.data); // If not first call, use existing data
|
||||
}
|
||||
if (PartSys == nullptr || SEGMENT.vHeight() < 2 || SEGMENT.vWidth() < 2) {
|
||||
FX_FALLBACK_STATIC;
|
||||
}
|
||||
|
||||
PartSys->updateSystem(); // Update system properties (dimensions and data pointers)
|
||||
|
||||
auto has_fallen_off_screen = [PartSys](uint32_t particleIndex) {
|
||||
return particleIndex < PartSys->numSources
|
||||
? PartSys->sources[particleIndex].source.y < PartSys->maxY * -1
|
||||
: true;
|
||||
};
|
||||
|
||||
// This will be SEGMENT.vWidth() unless the particle system had insufficient memory
|
||||
uint32_t numComets = PartSys->numSources;
|
||||
// Pick a random column for a new comet to spawn, but reset it to null if it's not time yet or there's already a
|
||||
// comet nearby
|
||||
uint32_t chosenIndex = hw_random(numComets);
|
||||
if (
|
||||
strip.now < nextCometCreationTime
|
||||
|| !has_fallen_off_screen(chosenIndex - 1)
|
||||
|| !has_fallen_off_screen(chosenIndex)
|
||||
|| !has_fallen_off_screen(chosenIndex + 1)
|
||||
) {
|
||||
chosenIndex = NULL_INDEX;
|
||||
} else {
|
||||
uint16_t cometFrequencyDelay = 2040 - (SEGMENT.intensity << 3);
|
||||
nextCometCreationTime = strip.now + cometFrequencyDelay + hw_random16(cometFrequencyDelay);
|
||||
}
|
||||
uint8_t canLargeCometSpawn =
|
||||
// Slider 3 determines % of large comets with extra particle sources on their sides
|
||||
SEGMENT.custom1 > hw_random8(254)
|
||||
&& chosenIndex != 0
|
||||
&& chosenIndex != numComets - 1;
|
||||
uint8_t fallingSpeed = 1 + (SEGMENT.speed >> 2);
|
||||
|
||||
// Update the comets
|
||||
for (i = 0; i < numComets; i++) {
|
||||
auto& source = PartSys->sources[i];
|
||||
auto& sourceParticle = source.source;
|
||||
|
||||
if (!has_fallen_off_screen(i)) {
|
||||
// Active comets fall downwards and emit flames
|
||||
sourceParticle.y -= fallingSpeed;
|
||||
source.vy = (SEGMENT.speed >> 5) - fallingSpeed; // Emitting speed (upwards)
|
||||
PartSys->flameEmit(PartSys->sources[i]);
|
||||
continue;
|
||||
}
|
||||
|
||||
bool isChosenComet = i == chosenIndex;
|
||||
bool isChosenSideComet =
|
||||
canLargeCometSpawn &&
|
||||
(i == chosenIndex - 1 || i == chosenIndex + 1);
|
||||
|
||||
// Chosen comets respawn at the top
|
||||
if (isChosenComet || isChosenSideComet) {
|
||||
// Map the comet index into an output pixel index
|
||||
sourceParticle.x = i * PartSys->maxX / (SEGMENT.vWidth() - 1);
|
||||
// Spawn a bit above the top to avoid popping into view
|
||||
sourceParticle.y = PartSys->maxY + (2 * fallingSpeed);
|
||||
if (isChosenComet) {
|
||||
// Slider 4 controls comet length via particle lifetime and fire intensity adjustments
|
||||
source.maxLife = 16 + (SEGMENT.custom2 >> 2);
|
||||
source.minLife = source.maxLife >> 1;
|
||||
sourceParticle.ttl = 16 - (SEGMENT.custom2 >> 4);
|
||||
} else {
|
||||
// Side comets have fixed length
|
||||
source.maxLife = 18;
|
||||
source.minLife = 14;
|
||||
sourceParticle.ttl = 16;
|
||||
// Shift side comets up by 1 pixel
|
||||
sourceParticle.y += 2 * PartSys->maxY / (SEGMENT.vHeight() - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Slider 4 controls comet length via particle lifetime and fire intensity adjustments
|
||||
PartSys->updateFire(max(255U - SEGMENT.custom2, 45U));
|
||||
}
|
||||
static const char _data_FX_MODE_PSCOMET[] PROGMEM = "PS Comet@Falling Speed,Comet Frequency,Large Comet Probability,Comet Length;;!;2;pal=35,sx=128,ix=255,c1=32,c2=128";
|
||||
|
||||
/////////////////////
|
||||
// UserMod Class //
|
||||
/////////////////////
|
||||
|
||||
class PSCometUsermod : public Usermod {
|
||||
public:
|
||||
void setup() override {
|
||||
strip.addEffect(255, &mode_pscomet, _data_FX_MODE_PSCOMET);
|
||||
}
|
||||
|
||||
void loop() override {}
|
||||
};
|
||||
|
||||
static PSCometUsermod ps_comet;
|
||||
REGISTER_USERMOD(ps_comet);
|
||||
@@ -1,25 +0,0 @@
|
||||
## Description
|
||||
|
||||
A 2D falling comet effect similar to "Matrix" but with a fire particle simulation to enhance the comet trail visuals. Works with custom color palettes, defaulting to "Fire". Supports "small" and "large" comets which are 1px and 3px wide respectively.
|
||||
|
||||
Demo: [https://imgur.com/a/i1v5WAy](https://imgur.com/a/i1v5WAy)
|
||||
|
||||
## Installation
|
||||
|
||||
To activate the usermod, add the following line to your platformio_override.ini
|
||||
```ini
|
||||
custom_usermods = ps_comet
|
||||
```
|
||||
Or if you are already using a usermod, append ps_comet to the list
|
||||
```ini
|
||||
custom_usermods = audioreactive ps_comet
|
||||
```
|
||||
|
||||
You should now see "PS Comet" appear in your effect list.
|
||||
|
||||
## Parameters
|
||||
|
||||
1. **Falling Speed** sets how fast the comets fall
|
||||
2. **Comet Frequency** determines how many comets are on screen at a time
|
||||
3. **Large Comet Probability** determines how often large 3px wide comets spawn
|
||||
4. **Comet Length** sets how far comet trails stretch vertically
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"name": "PS Comet",
|
||||
"build": { "libArchive": false }
|
||||
}
|
||||
@@ -129,14 +129,11 @@ class PWMFanUsermod : public Usermod {
|
||||
if (pwmChannel == 255) { //no more free LEDC channels
|
||||
deinitPWMfan(); return;
|
||||
}
|
||||
// configure LED PWM functionalitites - ESP-IDF 5.x API
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)
|
||||
ledcAttachChannel(pwmPin, 25000, 8, pwmChannel); // New API: ledcAttach(pin, freq, resolution, channel); keep PinManager channel ownership consistent
|
||||
#else
|
||||
// configure LED PWM functionalitites
|
||||
ledcSetup(pwmChannel, 25000, 8);
|
||||
// attach the channel to the GPIO to be controlled
|
||||
ledcAttachPin(pwmPin, pwmChannel);
|
||||
#endif
|
||||
#endif
|
||||
DEBUG_PRINTLN(F("Fan PWM sucessfully initialized."));
|
||||
}
|
||||
|
||||
|
||||
+6
-6
@@ -1,5 +1,6 @@
|
||||
; Options
|
||||
; -------
|
||||
; USERMOD_SN_PHOTORESISTOR - define this to have this user mod included wled00\usermods_list.cpp
|
||||
; USERMOD_SN_PHOTORESISTOR_MEASUREMENT_INTERVAL - the number of milliseconds between measurements, defaults to 60 seconds
|
||||
; USERMOD_SN_PHOTORESISTOR_FIRST_MEASUREMENT_AT - the number of milliseconds after boot to take first measurement, defaults to 20 seconds
|
||||
; USERMOD_SN_PHOTORESISTOR_REFERENCE_VOLTAGE - the voltage supplied to the sensor, defaults to 5v
|
||||
@@ -7,10 +8,9 @@
|
||||
; USERMOD_SN_PHOTORESISTOR_RESISTOR_VALUE - the resistor size, defaults to 10000.0 (10K hms)
|
||||
; USERMOD_SN_PHOTORESISTOR_OFFSET_VALUE - the offset value to report on, defaults to 25
|
||||
;
|
||||
[env:usermod_sn_photoresistor_esp8266_2m]
|
||||
extends = env:esp8266_2m
|
||||
custom_usermods = ${env:esp8266_2m.custom_usermods} SN_Photoresistor
|
||||
[env:usermod_sn_photoresistor_d1_mini]
|
||||
extends = env:d1_mini
|
||||
build_flags =
|
||||
${env:esp8266_2m.build_flags}
|
||||
-D USERMOD_SN_PHOTORESISTOR_MEASUREMENT_INTERVAL=60
|
||||
lib_deps = ${env:esp8266_2m.lib_deps}
|
||||
${common.build_flags_esp8266}
|
||||
-D USERMOD_SN_PHOTORESISTOR
|
||||
lib_deps = ${env.lib_deps}
|
||||
@@ -196,7 +196,7 @@ class St7789DisplayUsermod : public Usermod {
|
||||
|
||||
// Check if values which are shown on display changed from the last time.
|
||||
if ((((apActive) ? String(apSSID) : WiFi.SSID()) != knownSsid) ||
|
||||
(knownIp != (apActive ? IPAddress(4, 3, 2, 1) : WLEDNetwork.localIP())) ||
|
||||
(knownIp != (apActive ? IPAddress(4, 3, 2, 1) : Network.localIP())) ||
|
||||
(knownBrightness != bri) ||
|
||||
(knownEffectSpeed != strip.getMainSegment().speed) ||
|
||||
(knownEffectIntensity != strip.getMainSegment().intensity) ||
|
||||
@@ -225,7 +225,7 @@ class St7789DisplayUsermod : public Usermod {
|
||||
#else
|
||||
knownSsid = WiFi.SSID();
|
||||
#endif
|
||||
knownIp = apActive ? IPAddress(4, 3, 2, 1) : WLEDNetwork.localIP();
|
||||
knownIp = apActive ? IPAddress(4, 3, 2, 1) : WiFi.localIP();
|
||||
knownBrightness = bri;
|
||||
knownMode = strip.getMainSegment().mode;
|
||||
knownPalette = strip.getMainSegment().palette;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"name": "ST7789_display",
|
||||
"name:": "ST7789_display",
|
||||
"build": { "libArchive": false }
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
[env:esp32dev]
|
||||
build_flags = ${common.build_flags_esp32}
|
||||
; PIN defines - uncomment and change, if needed:
|
||||
; -D LEDPIN=2
|
||||
-D BTNPIN=35
|
||||
; -D IRPIN=4
|
||||
; -D RLYPIN=12
|
||||
; -D RLYMDE=1
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "UsermodTemperature.h"
|
||||
|
||||
static void mode_temperature();
|
||||
static uint16_t mode_temperature();
|
||||
|
||||
//Dallas sensor quick (& dirty) reading. Credit to - Author: Peter Scargill, August 17th, 2013
|
||||
float UsermodTemperature::readDallas() {
|
||||
@@ -20,19 +20,17 @@ float UsermodTemperature::readDallas() {
|
||||
}
|
||||
#endif
|
||||
switch(sensorFound) {
|
||||
case 0x10: // DS18S20 has 9-bit precision - 1-bit fraction part
|
||||
case 0x10: // DS18S20 has 9-bit precision
|
||||
result = (data[1] << 8) | data[0];
|
||||
retVal = float(result) * 0.5f;
|
||||
break;
|
||||
case 0x22: // DS1822
|
||||
case 0x28: // DS18B20
|
||||
case 0x22: // DS18B20
|
||||
case 0x28: // DS1822
|
||||
case 0x3B: // DS1825
|
||||
case 0x42: // DS28EA00
|
||||
// 12-bit precision - 4-bit fraction part
|
||||
result = (data[1] << 8) | data[0];
|
||||
// Clear LSBs to match desired precision (9/10/11-bit) rounding towards negative infinity
|
||||
result &= 0xFFFF << (3 - (resolution & 3));
|
||||
retVal = float(result) * 0.0625f; // 2^(-4)
|
||||
result = (data[1]<<4) | (data[0]>>4); // we only need whole part, we will add fraction when returning
|
||||
if (data[1] & 0x80) result |= 0xF000; // fix negative value
|
||||
retVal = float(result) + ((data[0] & 0x08) ? 0.5f : 0.0f);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -71,8 +69,8 @@ bool UsermodTemperature::findSensor() {
|
||||
if (oneWire->crc8(deviceAddress, 7) == deviceAddress[7]) {
|
||||
switch (deviceAddress[0]) {
|
||||
case 0x10: // DS18S20
|
||||
case 0x22: // DS1822
|
||||
case 0x28: // DS18B20
|
||||
case 0x22: // DS18B20
|
||||
case 0x28: // DS1822
|
||||
case 0x3B: // DS1825
|
||||
case 0x42: // DS28EA00
|
||||
DEBUG_PRINTLN(F("Sensor found."));
|
||||
@@ -279,7 +277,6 @@ void UsermodTemperature::addToConfig(JsonObject &root) {
|
||||
top[FPSTR(_parasite)] = parasite;
|
||||
top[FPSTR(_parasitePin)] = parasitePin;
|
||||
top[FPSTR(_domoticzIDX)] = idx;
|
||||
top[FPSTR(_resolution)] = resolution;
|
||||
DEBUG_PRINTLN(F("Temperature config saved."));
|
||||
}
|
||||
|
||||
@@ -307,7 +304,6 @@ bool UsermodTemperature::readFromConfig(JsonObject &root) {
|
||||
parasite = top[FPSTR(_parasite)] | parasite;
|
||||
parasitePin = top[FPSTR(_parasitePin)] | parasitePin;
|
||||
idx = top[FPSTR(_domoticzIDX)] | idx;
|
||||
resolution = top[FPSTR(_resolution)] | resolution;
|
||||
|
||||
if (!initDone) {
|
||||
// first run: reading from cfg.json
|
||||
@@ -328,7 +324,7 @@ bool UsermodTemperature::readFromConfig(JsonObject &root) {
|
||||
}
|
||||
}
|
||||
// use "return !top["newestParameter"].isNull();" when updating Usermod with new features
|
||||
return !top[FPSTR(_resolution)].isNull();
|
||||
return !top[FPSTR(_domoticzIDX)].isNull();
|
||||
}
|
||||
|
||||
void UsermodTemperature::appendConfigData() {
|
||||
@@ -336,14 +332,6 @@ void UsermodTemperature::appendConfigData() {
|
||||
oappend(F("',1,'<i>(if no Vcc connected)</i>');")); // 0 is field type, 1 is actual field
|
||||
oappend(F("addInfo('")); oappend(String(FPSTR(_name)).c_str()); oappend(F(":")); oappend(String(FPSTR(_parasitePin)).c_str());
|
||||
oappend(F("',1,'<i>(for external MOSFET)</i>');")); // 0 is field type, 1 is actual field
|
||||
oappend(F("dd=addDD('")); oappend(String(FPSTR(_name)).c_str());
|
||||
oappend(F("','")); oappend(String(FPSTR(_resolution)).c_str()); oappend(F("');"));
|
||||
oappend(F("addO(dd,'0.5 °C (9-bit)',0);"));
|
||||
oappend(F("addO(dd,'0.25°C (10-bit)',1);"));
|
||||
oappend(F("addO(dd,'0.125°C (11-bit)',2);"));
|
||||
oappend(F("addO(dd,'0.0625°C (12-bit)',3);"));
|
||||
oappend(F("addInfo('")); oappend(String(FPSTR(_name)).c_str()); oappend(F(":")); oappend(String(FPSTR(_resolution)).c_str());
|
||||
oappend(F("',1,'<i>(ignored on DS18S20)</i>');")); // 0 is field type, 1 is actual field
|
||||
}
|
||||
|
||||
float UsermodTemperature::getTemperature() {
|
||||
@@ -363,18 +351,18 @@ const char UsermodTemperature::_readInterval[] PROGMEM = "read-interval-s";
|
||||
const char UsermodTemperature::_parasite[] PROGMEM = "parasite-pwr";
|
||||
const char UsermodTemperature::_parasitePin[] PROGMEM = "parasite-pwr-pin";
|
||||
const char UsermodTemperature::_domoticzIDX[] PROGMEM = "domoticz-idx";
|
||||
const char UsermodTemperature::_resolution[] PROGMEM = "resolution";
|
||||
const char UsermodTemperature::_sensor[] PROGMEM = "sensor";
|
||||
const char UsermodTemperature::_temperature[] PROGMEM = "temperature";
|
||||
const char UsermodTemperature::_Temperature[] PROGMEM = "/temperature";
|
||||
const char UsermodTemperature::_data_fx[] PROGMEM = "Temperature@Min,Max;;!;01;pal=54,sx=255,ix=0";
|
||||
|
||||
static void mode_temperature() {
|
||||
static uint16_t mode_temperature() {
|
||||
float low = roundf(mapf((float)SEGMENT.speed, 0.f, 255.f, -150.f, 150.f)); // default: 15°C, range: -15°C to 15°C
|
||||
float high = roundf(mapf((float)SEGMENT.intensity, 0.f, 255.f, 300.f, 600.f)); // default: 30°C, range 30°C to 60°C
|
||||
float temp = constrain(UsermodTemperature::getInstance()->getTemperatureC()*10.f, low, high); // get a little better resolution (*10)
|
||||
unsigned i = map(roundf(temp), (unsigned)low, (unsigned)high, 0, 248);
|
||||
SEGMENT.fill(SEGMENT.color_from_palette(i, false, false, 255));
|
||||
return FRAMETIME;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -48,7 +48,6 @@ class UsermodTemperature : public Usermod {
|
||||
|
||||
bool HApublished = false;
|
||||
int16_t idx = -1; // Domoticz virtual sensor idx
|
||||
uint8_t resolution = 0; // 9bits=0, 10bits=1, 11bits=2, 12bits=3
|
||||
|
||||
// strings to reduce flash memory usage (used more than twice)
|
||||
static const char _name[];
|
||||
@@ -57,7 +56,6 @@ class UsermodTemperature : public Usermod {
|
||||
static const char _parasite[];
|
||||
static const char _parasitePin[];
|
||||
static const char _domoticzIDX[];
|
||||
static const char _resolution[];
|
||||
static const char _sensor[];
|
||||
static const char _temperature[];
|
||||
static const char _Temperature[];
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
; Options
|
||||
; -------
|
||||
; USERMOD_DALLASTEMPERATURE_MEASUREMENT_INTERVAL - the number of milliseconds between measurements, defaults to 60 seconds
|
||||
;
|
||||
|
||||
@@ -5,12 +5,9 @@
|
||||
#include "tetrisaigame.h"
|
||||
// By: muebau
|
||||
|
||||
bool noFlashOnClear = false;
|
||||
|
||||
typedef struct TetrisAI_data
|
||||
{
|
||||
unsigned long lastTime = 0;
|
||||
unsigned long clearingStartTime = 0;
|
||||
TetrisAIGame tetris;
|
||||
uint8_t intelligence;
|
||||
uint8_t rotate;
|
||||
@@ -34,27 +31,16 @@ void drawGrid(TetrisAIGame* tetris, TetrisAI_data* tetrisai_data)
|
||||
//GRID
|
||||
for (auto index_y = 4; index_y < tetris->grid.height; index_y++)
|
||||
{
|
||||
bool isRowClearing = tetris->grid.gridBW.clearingRows[index_y];
|
||||
for (auto index_x = 0; index_x < tetris->grid.width; index_x++)
|
||||
{
|
||||
CRGB color;
|
||||
uint8_t gridPixel = *tetris->grid.getPixel(index_x, index_y);
|
||||
if (isRowClearing) {
|
||||
if (noFlashOnClear) {
|
||||
color = CRGB::Gray;
|
||||
} else {
|
||||
//flash color white and black every 200ms
|
||||
color = (strip.now % 200) < 150
|
||||
? CRGB::Gray
|
||||
: CRGB::Black;
|
||||
}
|
||||
}
|
||||
else if (gridPixel == 0) {
|
||||
if (*tetris->grid.getPixel(index_x, index_y) == 0)
|
||||
{
|
||||
//BG color
|
||||
color = SEGCOLOR(1);
|
||||
}
|
||||
//game over animation
|
||||
else if (gridPixel == 254)
|
||||
else if(*tetris->grid.getPixel(index_x, index_y) == 254)
|
||||
{
|
||||
//use fg
|
||||
color = SEGCOLOR(0);
|
||||
@@ -62,7 +48,7 @@ void drawGrid(TetrisAIGame* tetris, TetrisAI_data* tetrisai_data)
|
||||
else
|
||||
{
|
||||
//spread the color over the whole palette
|
||||
uint8_t colorIndex = gridPixel * 32;
|
||||
uint8_t colorIndex = *tetris->grid.getPixel(index_x, index_y) * 32;
|
||||
colorIndex += tetrisai_data->colorOffset;
|
||||
color = ColorFromPalette(SEGPALETTE, colorIndex, 255, NOBLEND);
|
||||
}
|
||||
@@ -112,13 +98,13 @@ void drawGrid(TetrisAIGame* tetris, TetrisAI_data* tetrisai_data)
|
||||
////////////////////////////
|
||||
// 2D Tetris AI //
|
||||
////////////////////////////
|
||||
void mode_2DTetrisAI()
|
||||
uint16_t mode_2DTetrisAI()
|
||||
{
|
||||
if (!strip.isMatrix || !SEGENV.allocateData(sizeof(tetrisai_data)))
|
||||
{
|
||||
// not a 2D set-up
|
||||
SEGMENT.fill(SEGCOLOR(0));
|
||||
return;
|
||||
return 350;
|
||||
}
|
||||
TetrisAI_data* tetrisai_data = reinterpret_cast<TetrisAI_data*>(SEGENV.data);
|
||||
|
||||
@@ -184,7 +170,6 @@ void mode_2DTetrisAI()
|
||||
|
||||
tetrisai_data->tetris = TetrisAIGame(gridWidth, gridHeight, nLookAhead, piecesData, numPieces);
|
||||
tetrisai_data->tetris.state = TetrisAIGame::States::INIT;
|
||||
tetrisai_data->clearingStartTime = 0;
|
||||
SEGMENT.fill(SEGCOLOR(1));
|
||||
}
|
||||
|
||||
@@ -199,21 +184,7 @@ void mode_2DTetrisAI()
|
||||
tetrisai_data->tetris.ai.bumpiness = -0.184483f + dui;
|
||||
}
|
||||
|
||||
//end line clearing flashing effect if needed
|
||||
if (tetrisai_data->tetris.grid.gridBW.hasClearingRows())
|
||||
{
|
||||
if (tetrisai_data->clearingStartTime == 0) {
|
||||
tetrisai_data->clearingStartTime = strip.now;
|
||||
}
|
||||
if (strip.now - tetrisai_data->clearingStartTime > 750)
|
||||
{
|
||||
tetrisai_data->tetris.grid.gridBW.clearedLinesReadyForRemoval = true;
|
||||
tetrisai_data->tetris.grid.cleanupFullLines();
|
||||
tetrisai_data->clearingStartTime = 0;
|
||||
}
|
||||
drawGrid(&tetrisai_data->tetris, tetrisai_data);
|
||||
}
|
||||
else if (tetrisai_data->tetris.state == TetrisAIGame::ANIMATE_MOVE)
|
||||
if (tetrisai_data->tetris.state == TetrisAIGame::ANIMATE_MOVE)
|
||||
{
|
||||
|
||||
if (strip.now - tetrisai_data->lastTime > msDelayMove)
|
||||
@@ -251,6 +222,8 @@ void mode_2DTetrisAI()
|
||||
{
|
||||
tetrisai_data->tetris.poll();
|
||||
}
|
||||
|
||||
return FRAMETIME;
|
||||
} // mode_2DTetrisAI()
|
||||
static const char _data_FX_MODE_2DTETRISAI[] PROGMEM = "Tetris AI@!,Look ahead,Intelligence,Rotate color,Mistake free,Show next,Border,Mistakes;Game Over,!,Border;!;2;sx=127,ix=64,c1=255,c2=0,c3=31,o1=1,o2=1,o3=0,pal=11";
|
||||
|
||||
@@ -258,7 +231,6 @@ class TetrisAIUsermod : public Usermod
|
||||
{
|
||||
|
||||
private:
|
||||
static const char _name[];
|
||||
|
||||
public:
|
||||
void setup()
|
||||
@@ -266,20 +238,6 @@ public:
|
||||
strip.addEffect(255, &mode_2DTetrisAI, _data_FX_MODE_2DTETRISAI);
|
||||
}
|
||||
|
||||
void addToConfig(JsonObject& root) override
|
||||
{
|
||||
JsonObject top = root.createNestedObject(FPSTR(_name));
|
||||
top["noFlashOnClear"] = noFlashOnClear;
|
||||
}
|
||||
|
||||
bool readFromConfig(JsonObject& root) override
|
||||
{
|
||||
JsonObject top = root[FPSTR(_name)];
|
||||
bool configComplete = !top.isNull();
|
||||
configComplete &= getJsonValue(top["noFlashOnClear"], noFlashOnClear);
|
||||
return configComplete;
|
||||
}
|
||||
|
||||
void loop()
|
||||
{
|
||||
|
||||
@@ -291,7 +249,6 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
const char TetrisAIUsermod::_name[] PROGMEM = "TetrisAI_v2";
|
||||
|
||||
static TetrisAIUsermod tetrisai_v2;
|
||||
REGISTER_USERMOD(tetrisai_v2);
|
||||
@@ -13,6 +13,7 @@
|
||||
#ifndef __GRIDBW_H__
|
||||
#define __GRIDBW_H__
|
||||
|
||||
#include <iterator>
|
||||
#include <vector>
|
||||
#include "pieces.h"
|
||||
|
||||
@@ -25,18 +26,11 @@ public:
|
||||
uint8_t width;
|
||||
uint8_t height;
|
||||
std::vector<uint32_t> pixels;
|
||||
// When a row fills, we mark it here first so it can flash before being
|
||||
// fully removed.
|
||||
std::vector<bool> clearingRows;
|
||||
// True when a line clearing flashing effect is over and we're ready to
|
||||
// fully clean up the lines
|
||||
bool clearedLinesReadyForRemoval = false;
|
||||
|
||||
GridBW(uint8_t width, uint8_t height):
|
||||
width(width),
|
||||
height(height),
|
||||
pixels(height),
|
||||
clearingRows(height)
|
||||
pixels(height)
|
||||
{
|
||||
if (width > 32)
|
||||
{
|
||||
@@ -91,26 +85,9 @@ public:
|
||||
piece->landingY = piece->landingY > 0 ? piece->landingY - 1 : 0;
|
||||
}
|
||||
|
||||
bool hasClearingRows()
|
||||
{
|
||||
for (bool rowClearing : clearingRows)
|
||||
{
|
||||
if (rowClearing)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void cleanupFullLines()
|
||||
{
|
||||
// Skip cleanup if there are rows clearing
|
||||
if (hasClearingRows() && !clearedLinesReadyForRemoval) {
|
||||
return;
|
||||
}
|
||||
uint8_t offset = 0;
|
||||
bool doneRemovingClearedLines = false;
|
||||
|
||||
//from "height - 1" to "0", so from bottom row to top
|
||||
for (uint8_t row = height; row-- > 0; )
|
||||
@@ -118,13 +95,8 @@ public:
|
||||
//full line?
|
||||
if (isLineFull(row))
|
||||
{
|
||||
if (clearedLinesReadyForRemoval) {
|
||||
offset++;
|
||||
pixels[row] = 0x0;
|
||||
doneRemovingClearedLines = true;
|
||||
} else {
|
||||
clearingRows[row] = true;
|
||||
}
|
||||
offset++;
|
||||
pixels[row] = 0x0;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -134,20 +106,11 @@ public:
|
||||
pixels[row] = 0x0;
|
||||
}
|
||||
}
|
||||
if (doneRemovingClearedLines) {
|
||||
clearingRows.assign(height, false);
|
||||
clearedLinesReadyForRemoval = false;
|
||||
}
|
||||
}
|
||||
|
||||
bool isLineFull(uint8_t y)
|
||||
{
|
||||
return pixels[y] == (width >= 32 ? UINT32_MAX : (1U << width) - 1);
|
||||
}
|
||||
|
||||
bool isLineReadyForRemoval(uint8_t y)
|
||||
{
|
||||
return clearedLinesReadyForRemoval && isLineFull(y);
|
||||
return pixels[y] == (uint32_t)((1 << width) - 1);
|
||||
}
|
||||
|
||||
void reset()
|
||||
@@ -159,8 +122,6 @@ public:
|
||||
|
||||
pixels.clear();
|
||||
pixels.resize(height);
|
||||
clearingRows.assign(height, false);
|
||||
clearedLinesReadyForRemoval = false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ public:
|
||||
//from "height - 1" to "0", so from bottom row to top
|
||||
for (uint8_t y = height; y-- > 0; )
|
||||
{
|
||||
if (gridBW.isLineReadyForRemoval(y))
|
||||
if (gridBW.isLineFull(y))
|
||||
{
|
||||
offset++;
|
||||
for (uint8_t x = 0; x < width; x++)
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#include <bitset>
|
||||
#include <cstddef>
|
||||
#include <cassert>
|
||||
#include <iostream>
|
||||
|
||||
#define numPieces 7
|
||||
|
||||
|
||||
@@ -2,16 +2,13 @@
|
||||
|
||||
This usermod adds a self-playing Tetris game as an 'effect'. The mod requires version 0.14 or higher as it relies on matrix support. The effect was tested on an ESP32 4MB with a WS2812B 16x16 matrix.
|
||||
|
||||
PHOTOSENSITIVE EPILEPSY WARNING: By default the effect features a flashing animation on line clear. This can be disabled
|
||||
from the usermod settings page in WLED.
|
||||
Version 1.0
|
||||
|
||||
## Installation
|
||||
|
||||
To activate the usermod, add the following line to your platformio_override.ini
|
||||
`custom_usermods = tetrisai_v2`
|
||||
The effect will then become available under the name 'Tetris AI'. If you are running out of flash memory, use a different memory layout (e.g. [WLED_ESP32_4MB_256KB_FS.csv](https://github.com/wled-dev/WLED/blob/main/tools/WLED_ESP32_4MB_256KB_FS.csv)).
|
||||
Just activate the usermod with `-D USERMOD_TETRISAI` and the effect will become available under the name 'Tetris AI'. If you are running out of flash memory, use a different memory layout (e.g. [WLED_ESP32_4MB_256KB_FS.csv](https://github.com/wled-dev/WLED/blob/main/tools/WLED_ESP32_4MB_256KB_FS.csv)).
|
||||
|
||||
If needed simply add to `platformio_override.ini`:
|
||||
If needed simply add to `platformio_override.ini` (or `platformio_override.ini`):
|
||||
|
||||
```ini
|
||||
board_build.partitions = tools/WLED_ESP32_4MB_256KB_FS.csv
|
||||
|
||||
@@ -68,7 +68,7 @@ public:
|
||||
}
|
||||
|
||||
//line full if all ones in mask :-)
|
||||
if (grid.isLineReadyForRemoval(row))
|
||||
if (grid.isLineFull(row))
|
||||
{
|
||||
rating->fullLines++;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
#include <stdint.h>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
|
||||
#include "tetrisbag.h"
|
||||
|
||||
@@ -86,12 +87,17 @@ public:
|
||||
void queuePiece()
|
||||
{
|
||||
//move vector to left
|
||||
for (uint8_t i = 1; i < piecesQueue.size(); i++) {
|
||||
piecesQueue[i - 1] = piecesQueue[i];
|
||||
}
|
||||
std::rotate(piecesQueue.begin(), piecesQueue.begin() + 1, piecesQueue.end());
|
||||
piecesQueue[piecesQueue.size() - 1] = getNextPiece();
|
||||
}
|
||||
|
||||
void queuePiece(uint8_t idx)
|
||||
{
|
||||
//move vector to left
|
||||
std::rotate(piecesQueue.begin(), piecesQueue.begin() + 1, piecesQueue.end());
|
||||
piecesQueue[piecesQueue.size() - 1] = Piece(idx % nPieces);
|
||||
}
|
||||
|
||||
void reset()
|
||||
{
|
||||
bag.clear();
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
#include <driver/i2s.h>
|
||||
#include <driver/adc.h>
|
||||
|
||||
#ifdef WLED_ENABLE_DMX
|
||||
#error This audio reactive usermod is not compatible with DMX Out.
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
#if defined(ARDUINO_ARCH_ESP32) && (defined(WLED_DEBUG) || defined(SR_DEBUG))
|
||||
@@ -20,26 +24,6 @@
|
||||
* ....
|
||||
*/
|
||||
|
||||
#define FFT_PREFER_EXACT_PEAKS // use Blackman-Harris FFT windowing instead of Flat Top -> results in "sharper" peaks and less "leaking" into other frequencies (credits to @softhack)
|
||||
|
||||
/*
|
||||
* Note on FFT variants:
|
||||
* - ArduinoFFT: uses floating point calculations, very slow on S2 and C3 (no FPU)
|
||||
* - ESP-IDF DSP library:
|
||||
- faster but uses ~13k of extra flash on ESP32 and S3
|
||||
* - uses integer math on S2 and C3: slightly less accurate but over 10x faster than ArduinoFFT and uses less flash
|
||||
- not available in IDF < 4.4
|
||||
* - ArduinoFFT is used by default on ESP32 and S3
|
||||
* - ESP-IDF DSP FFT with integer math is used by default on S2 and C3
|
||||
* - defines:
|
||||
* - UM_AUDIOREACTIVE_USE_ARDUINO_FFT: use ArduinoFFT library for FFT
|
||||
* - UM_AUDIOREACTIVE_USE_ESPDSP_FFT: use ESP-IDF DSP for FFT
|
||||
*/
|
||||
|
||||
//#define UM_AUDIOREACTIVE_USE_ESPDSP_FFT // default on S2 and C3
|
||||
//#define UM_AUDIOREACTIVE_USE_INTEGER_FFT // use integer FFT if using ESP-IDF DSP library, always used on S2 and C3 (UM_AUDIOREACTIVE_USE_ARDUINO_FFT takes priority)
|
||||
//#define UM_AUDIOREACTIVE_USE_ARDUINO_FFT // default on ESP32 and S3
|
||||
|
||||
#if !defined(FFTTASK_PRIORITY)
|
||||
#define FFTTASK_PRIORITY 1 // standard: looptask prio
|
||||
//#define FFTTASK_PRIORITY 2 // above looptask, below asyc_tcp
|
||||
@@ -119,46 +103,6 @@ static uint8_t maxVol = 31; // (was 10) Reasonable value for constant v
|
||||
static uint8_t binNum = 8; // Used to select the bin for FFT based beat detection (deprecated)
|
||||
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
#if !defined(UM_AUDIOREACTIVE_USE_ESPDSP_FFT) && (defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32))
|
||||
#define UM_AUDIOREACTIVE_USE_ARDUINO_FFT // use ArduinoFFT library for FFT instead of ESP-IDF DSP library by default on ESP32 and S3
|
||||
#endif
|
||||
|
||||
#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(4, 4, 0)
|
||||
#define UM_AUDIOREACTIVE_USE_ARDUINO_FFT // DSP FFT library is not available in ESP-IDF < 4.4
|
||||
#endif
|
||||
|
||||
#ifdef UM_AUDIOREACTIVE_USE_ARDUINO_FFT
|
||||
#include <arduinoFFT.h> // ArduinoFFT library for FFT and window functions
|
||||
#undef UM_AUDIOREACTIVE_USE_INTEGER_FFT // arduinoFFT has not integer support
|
||||
#else
|
||||
#include "dsps_fft2r.h" // ESP-IDF DSP library for FFT and window functions
|
||||
#ifdef FFT_PREFER_EXACT_PEAKS
|
||||
#include "dsps_wind_blackman_harris.h"
|
||||
#else
|
||||
#include "dsps_wind_flat_top.h"
|
||||
#endif
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3)
|
||||
#define UM_AUDIOREACTIVE_USE_INTEGER_FFT // always use integer FFT on ESP32-S2 and ESP32-C3
|
||||
#endif
|
||||
#endif // UM_AUDIOREACTIVE_USE_ARDUINO_FFT
|
||||
|
||||
#if !defined(UM_AUDIOREACTIVE_USE_INTEGER_FFT)
|
||||
using FFTsampleType = float;
|
||||
using FFTmathType = float;
|
||||
#define FFTabs fabsf
|
||||
#else
|
||||
using FFTsampleType = int16_t;
|
||||
using FFTmathType = int32_t;
|
||||
#define FFTabs abs
|
||||
#endif
|
||||
// These are the input and output vectors. Input vectors receive computed results from FFT.
|
||||
static FFTsampleType* valFFT = nullptr;
|
||||
#ifdef UM_AUDIOREACTIVE_USE_ARDUINO_FFT
|
||||
static float* vImag = nullptr; // imaginary part of FFT results
|
||||
#endif
|
||||
|
||||
// pre-computed window function
|
||||
static FFTsampleType* windowFFT = nullptr;
|
||||
|
||||
// use audio source class (ESP32 specific)
|
||||
#include "audio_source.h"
|
||||
@@ -168,14 +112,14 @@ constexpr int BLOCK_SIZE = 128; // I2S buffer size (samples)
|
||||
// globals
|
||||
static uint8_t inputLevel = 128; // UI slider value
|
||||
#ifndef SR_SQUELCH
|
||||
static uint8_t soundSquelch = 10; // squelch value for volume reactive routines (config value)
|
||||
uint8_t soundSquelch = 10; // squelch value for volume reactive routines (config value)
|
||||
#else
|
||||
static uint8_t soundSquelch = SR_SQUELCH; // squelch value for volume reactive routines (config value)
|
||||
uint8_t soundSquelch = SR_SQUELCH; // squelch value for volume reactive routines (config value)
|
||||
#endif
|
||||
#ifndef SR_GAIN
|
||||
static uint8_t sampleGain = 60; // sample gain (config value)
|
||||
uint8_t sampleGain = 60; // sample gain (config value)
|
||||
#else
|
||||
static uint8_t sampleGain = SR_GAIN; // sample gain (config value)
|
||||
uint8_t sampleGain = SR_GAIN; // sample gain (config value)
|
||||
#endif
|
||||
// user settable options for FFTResult scaling
|
||||
static uint8_t FFTScalingMode = 3; // 0 none; 1 optimized logarithmic; 2 optimized linear; 3 optimized square root
|
||||
@@ -200,8 +144,8 @@ const float agcSampleSmooth[AGC_NUM_PRESETS] = { 1/12.f, 1/6.f, 1/16.f}; //
|
||||
// AGC presets end
|
||||
|
||||
static AudioSource *audioSource = nullptr;
|
||||
static bool useBandPassFilter = false; // if true, enables a hard cutoff bandpass filter. Applies after FFT.
|
||||
static bool useMicFilter = false; // if true, enables a IIR bandpass filter 80Hz-20Khz to remove noise. Applies before FFT.
|
||||
static bool useBandPassFilter = false; // if true, enables a bandpass filter 80Hz-16Khz to remove noise. Applies before FFT.
|
||||
|
||||
////////////////////
|
||||
// Begin FFT Code //
|
||||
////////////////////
|
||||
@@ -209,7 +153,7 @@ static bool useMicFilter = false; // if true, enables a
|
||||
// some prototypes, to ensure consistent interfaces
|
||||
static float fftAddAvg(int from, int to); // average of several FFT result bins
|
||||
void FFTcode(void * parameter); // audio processing task: read samples, run FFT, fill GEQ channels from FFT results
|
||||
static void runMicFilter(uint16_t numSamples, FFTsampleType *sampleBuffer);
|
||||
static void runMicFilter(uint16_t numSamples, float *sampleBuffer); // pre-filtering of raw samples (band-pass)
|
||||
static void postProcessFFTResults(bool noiseGateOpen, int numberOfChannels); // post-processing and post-amp of GEQ channels
|
||||
|
||||
static TaskHandle_t FFT_Task = nullptr;
|
||||
@@ -245,13 +189,13 @@ constexpr uint16_t samplesFFT = 512; // Samples in an FFT batch - Thi
|
||||
constexpr uint16_t samplesFFT_2 = 256; // meaningfull part of FFT results - only the "lower half" contains useful information.
|
||||
// the following are observed values, supported by a bit of "educated guessing"
|
||||
//#define FFT_DOWNSCALE 0.65f // 20kHz - downscaling factor for FFT results - "Flat-Top" window @20Khz, old freq channels
|
||||
#ifdef FFT_PREFER_EXACT_PEAKS
|
||||
#define FFT_DOWNSCALE 0.40f // downscaling factor for FFT results, RMS averaging for "Blackman-Harris" Window @22kHz (credit to MM)
|
||||
#else
|
||||
#define FFT_DOWNSCALE 0.46f // downscaling factor for FFT results - for "Flat-Top" window @22Khz, new freq channels
|
||||
#endif
|
||||
#define LOG_256 5.54517744f // log(256)
|
||||
|
||||
// These are the input and output vectors. Input vectors receive computed results from FFT.
|
||||
static float* vReal = nullptr; // FFT sample inputs / freq output - these are our raw result bins
|
||||
static float* vImag = nullptr; // imaginary parts
|
||||
|
||||
// Create FFT object
|
||||
// lib_deps += https://github.com/kosme/arduinoFFT#develop @ 1.9.2
|
||||
// these options actually cause slow-downs on all esp32 processors, don't use them.
|
||||
@@ -260,20 +204,16 @@ constexpr uint16_t samplesFFT_2 = 256; // meaningfull part of FFT resul
|
||||
// Below options are forcing ArduinoFFT to use sqrtf() instead of sqrt()
|
||||
// #define sqrt_internal sqrtf // see https://github.com/kosme/arduinoFFT/pull/83 - since v2.0.0 this must be done in build_flags
|
||||
|
||||
#include <arduinoFFT.h> // FFT object is created in FFTcode
|
||||
// Helper functions
|
||||
|
||||
// compute average of several FFT result bins
|
||||
static float fftAddAvg(int from, int to) {
|
||||
FFTmathType result = 0;
|
||||
float result = 0.0f;
|
||||
for (int i = from; i <= to; i++) {
|
||||
result += valFFT[i];
|
||||
result += vReal[i];
|
||||
}
|
||||
#if !defined(UM_AUDIOREACTIVE_USE_INTEGER_FFT)
|
||||
result = result * 0.0625; // divide by 16 to reduce magnitude. Want end result to be scaled linear and ~4096 max.
|
||||
#else
|
||||
result *= 32; // scale result to match float values. note: raw scaling value between float and int is 512, float version is scaled down by 16
|
||||
#endif
|
||||
return float(result) / float(to - from + 1); // return average as float
|
||||
return result / float(to - from + 1);
|
||||
}
|
||||
|
||||
//
|
||||
@@ -282,61 +222,18 @@ static float fftAddAvg(int from, int to) {
|
||||
void FFTcode(void * parameter)
|
||||
{
|
||||
DEBUGSR_PRINT("FFT started on core: "); DEBUGSR_PRINTLN(xPortGetCoreID());
|
||||
#ifdef UM_AUDIOREACTIVE_USE_ARDUINO_FFT
|
||||
|
||||
// allocate FFT buffers on first call
|
||||
if (valFFT == nullptr) valFFT = (float*) calloc(samplesFFT, sizeof(float));
|
||||
if (vImag == nullptr) vImag = (float*) calloc(samplesFFT, sizeof(float));
|
||||
if ((valFFT == nullptr) || (vImag == nullptr)) {
|
||||
if (vReal == nullptr) vReal = (float*) calloc(samplesFFT, sizeof(float));
|
||||
if (vImag == nullptr) vImag = (float*) calloc(samplesFFT, sizeof(float));
|
||||
if ((vReal == nullptr) || (vImag == nullptr)) {
|
||||
// something went wrong
|
||||
if (valFFT) free(valFFT); valFFT = nullptr;
|
||||
if (vReal) free(vReal); vReal = nullptr;
|
||||
if (vImag) free(vImag); vImag = nullptr;
|
||||
return;
|
||||
}
|
||||
// Create FFT object with weighing factor storage
|
||||
ArduinoFFT<float> FFT = ArduinoFFT<float>(valFFT, vImag, samplesFFT, SAMPLE_RATE, true);
|
||||
#elif !defined(UM_AUDIOREACTIVE_USE_INTEGER_FFT)
|
||||
// allocate and initialize FFT buffers on first call
|
||||
// note: free() is never used on these pointers. If it ever is implemented, this implementation can cause memory leaks (need to free raw pointers)
|
||||
if (valFFT == nullptr) {
|
||||
float* raw_buffer = (float*)heap_caps_malloc((2 * samplesFFT * sizeof(float)) + 16, MALLOC_CAP_8BIT);
|
||||
if ((raw_buffer == nullptr)) return; // something went wrong
|
||||
valFFT = (float*)(((uintptr_t)raw_buffer + 15) & ~15); // SIMD requires aligned memory to 16-byte boundary. note in IDF5 there is MALLOC_CAP_SIMD available
|
||||
}
|
||||
// create window
|
||||
if (windowFFT == nullptr) {
|
||||
float* raw_buffer = (float*)heap_caps_malloc((samplesFFT * sizeof(float)) + 16, MALLOC_CAP_8BIT);
|
||||
if ((raw_buffer == nullptr)) return; // something went wrong
|
||||
windowFFT = (float*)(((uintptr_t)raw_buffer + 15) & ~15); // SIMD requires aligned memory to 16-byte boundary
|
||||
}
|
||||
if (dsps_fft2r_init_fc32(NULL, samplesFFT) != ESP_OK) return; // initialize FFT tables
|
||||
// create window function for FFT
|
||||
#ifdef FFT_PREFER_EXACT_PEAKS
|
||||
dsps_wind_blackman_harris_f32(windowFFT, samplesFFT);
|
||||
#else
|
||||
dsps_wind_flat_top_f32(windowFFT, samplesFFT);
|
||||
#endif
|
||||
#else
|
||||
// allocate and initialize integer FFT buffers on first call
|
||||
if (valFFT == nullptr) valFFT = (int16_t*) calloc(sizeof(int16_t), samplesFFT * 2);
|
||||
if ((valFFT == nullptr)) return; // something went wrong
|
||||
// create window
|
||||
if (windowFFT == nullptr) windowFFT = (int16_t*) calloc(sizeof(int16_t), samplesFFT);
|
||||
if ((windowFFT == nullptr)) return; // something went wrong
|
||||
if (dsps_fft2r_init_sc16(NULL, samplesFFT) != ESP_OK) return; // initialize FFT tables
|
||||
// create window function for FFT
|
||||
float *windowFloat = (float*) calloc(sizeof(float), samplesFFT); // temporary buffer for window function
|
||||
if ((windowFloat == nullptr)) return; // something went wrong
|
||||
#ifdef FFT_PREFER_EXACT_PEAKS
|
||||
dsps_wind_blackman_harris_f32(windowFloat, samplesFFT);
|
||||
#else
|
||||
dsps_wind_flat_top_f32(windowFloat, samplesFFT);
|
||||
#endif
|
||||
// convert float window to 16-bit int
|
||||
for (int i = 0; i < samplesFFT; i++) {
|
||||
windowFFT[i] = (int16_t)(windowFloat[i] * 32767.0f);
|
||||
}
|
||||
free(windowFloat); // free temporary buffer
|
||||
#endif
|
||||
ArduinoFFT<float> FFT = ArduinoFFT<float>( vReal, vImag, samplesFFT, SAMPLE_RATE, true);
|
||||
|
||||
// see https://www.freertos.org/vtaskdelayuntil.html
|
||||
const TickType_t xFrequency = FFT_MIN_CYCLE * portTICK_PERIOD_MS;
|
||||
@@ -358,7 +255,8 @@ void FFTcode(void * parameter)
|
||||
#endif
|
||||
|
||||
// get a fresh batch of samples from I2S
|
||||
if (audioSource) audioSource->getSamples(valFFT, samplesFFT); // note: valFFT is used as a int16_t buffer on C3 and S2, could optimize RAM use by only allocating half the size (but makes code harder to read)
|
||||
if (audioSource) audioSource->getSamples(vReal, samplesFFT);
|
||||
memset(vImag, 0, samplesFFT * sizeof(float)); // set imaginary parts to 0
|
||||
|
||||
#if defined(WLED_DEBUG) || defined(SR_DEBUG)
|
||||
if (start < esp_timer_get_time()) { // filter out overflows
|
||||
@@ -370,15 +268,16 @@ void FFTcode(void * parameter)
|
||||
|
||||
xLastWakeTime = xTaskGetTickCount(); // update "last unblocked time" for vTaskDelay
|
||||
|
||||
// band pass filter - can reduce noise floor by a factor of 50 and avoid aliasing effects to base & high frequency bands
|
||||
// band pass filter - can reduce noise floor by a factor of 50
|
||||
// downside: frequencies below 100Hz will be ignored
|
||||
if (useMicFilter) runMicFilter(samplesFFT, valFFT);
|
||||
if (useBandPassFilter) runMicFilter(samplesFFT, vReal);
|
||||
|
||||
// find highest sample in the batch
|
||||
FFTsampleType maxSample = 0; // max sample from FFT batch
|
||||
float maxSample = 0.0f; // max sample from FFT batch
|
||||
for (int i=0; i < samplesFFT; i++) {
|
||||
// pick our our current mic sample - we take the max value from all samples that go into FFT
|
||||
if ((valFFT[i] <= (INT16_MAX - 1024)) && (valFFT[i] >= (INT16_MIN + 1024))) //skip extreme values - normally these are artefacts
|
||||
if (FFTabs(valFFT[i]) > maxSample) maxSample = FFTabs(valFFT[i]);
|
||||
if ((vReal[i] <= (INT16_MAX - 1024)) && (vReal[i] >= (INT16_MIN + 1024))) //skip extreme values - normally these are artefacts
|
||||
if (fabsf((float)vReal[i]) > maxSample) maxSample = fabsf((float)vReal[i]);
|
||||
}
|
||||
// release highest sample to volume reactive effects early - not strictly necessary here - could also be done at the end of the function
|
||||
// early release allows the filters (getSample() and agcAvg()) to work with fresh values - we will have matching gain and noise gate values when we want to process the FFT results.
|
||||
@@ -390,97 +289,32 @@ void FFTcode(void * parameter)
|
||||
if (sampleAvg > 0.25f) { // noise gate open means that FFT results will be used. Don't run FFT if results are not needed.
|
||||
#endif
|
||||
|
||||
#ifdef UM_AUDIOREACTIVE_USE_ARDUINO_FFT
|
||||
// run Arduino FFT (takes 3-5ms on ESP32, ~12ms on ESP32-S2, ~20ms on ESP32-C3)
|
||||
memset(vImag, 0, samplesFFT * sizeof(float)); // set imaginary parts to 0
|
||||
// run FFT (takes 3-5ms on ESP32, ~12ms on ESP32-S2)
|
||||
FFT.dcRemoval(); // remove DC offset
|
||||
#ifdef FFT_PREFER_EXACT_PEAKS
|
||||
FFT.windowing(FFTWindow::Blackman_Harris, FFTDirection::Forward); // Weigh data using "Blackman- Harris" window - sharp peaks due to excellent sideband rejection
|
||||
#else
|
||||
FFT.windowing( FFTWindow::Flat_top, FFTDirection::Forward); // Weigh data using "Flat Top" function - better amplitude accuracy
|
||||
#endif
|
||||
//FFT.windowing(FFTWindow::Blackman_Harris, FFTDirection::Forward); // Weigh data using "Blackman- Harris" window - sharp peaks due to excellent sideband rejection
|
||||
FFT.compute( FFTDirection::Forward ); // Compute FFT
|
||||
FFT.complexToMagnitude(); // Compute magnitudes
|
||||
valFFT[0] = 0; // The remaining DC offset on the signal produces a strong spike on position 0 that should be eliminated to avoid issues.
|
||||
FFT.majorPeak(&FFT_MajorPeak, &FFT_Magnitude); // let the effects know which freq was most dominant
|
||||
// note: scaling is done in fftAddAvg(), so we don't scale here
|
||||
#else
|
||||
// run run float DSP FFT (takes ~x ms on ESP32, ~x ms on ESP32-S2, , ~x ms on ESP32-C3) TODO: test and fill in these values
|
||||
// remove DC offset
|
||||
FFTmathType sum = 0;
|
||||
for (int i = 0; i < samplesFFT; i++) sum += valFFT[i];
|
||||
FFTmathType mean = sum / (FFTmathType)samplesFFT;
|
||||
for (int i = 0; i < samplesFFT; i++) valFFT[i] -= mean;
|
||||
#if !defined(UM_AUDIOREACTIVE_USE_INTEGER_FFT)
|
||||
//apply window function to samples and fill buffer with interleaved complex values [Re,Im,Re,Im,...]
|
||||
for (int i = samplesFFT - 1; i >= 0 ; i--) {
|
||||
// fill the buffer back to front to avoid overwriting samples
|
||||
float windowed_sample = valFFT[i] * windowFFT[i];
|
||||
valFFT[i * 2] = windowed_sample;
|
||||
valFFT[i * 2 + 1] = 0.0; // set imaginary part to zero
|
||||
}
|
||||
#ifdef CONFIG_IDF_TARGET_ESP32S3
|
||||
dsps_fft2r_fc32_aes3(valFFT, samplesFFT); // ESP32 S3 optimized version of FFT
|
||||
#elif defined(CONFIG_IDF_TARGET_ESP32)
|
||||
dsps_fft2r_fc32_ae32(valFFT, samplesFFT); // ESP32 optimized version of FFT
|
||||
#else
|
||||
dsps_fft2r_fc32_ansi(valFFT, samplesFFT); // perform FFT using ANSI C implementation
|
||||
#endif
|
||||
dsps_bit_rev_fc32(valFFT, samplesFFT); // bit reverse
|
||||
valFFT[0] = 0; // set DC bin to 0, as it is not needed and can cause issues
|
||||
// convert to magnitude & find FFT_MajorPeak and FFT_Magnitude
|
||||
FFT_MajorPeak = 0;
|
||||
FFT_Magnitude = 0;
|
||||
for (int i = 1; i < samplesFFT_2; i++) { // skip [0] as it is DC offset
|
||||
float real_part = valFFT[i * 2];
|
||||
float imag_part = valFFT[i * 2 + 1];
|
||||
valFFT[i] = sqrtf(real_part * real_part + imag_part * imag_part);
|
||||
if (valFFT[i] > FFT_Magnitude) {
|
||||
FFT_Magnitude = valFFT[i];
|
||||
FFT_MajorPeak = i*(SAMPLE_RATE/samplesFFT);
|
||||
}
|
||||
// note: scaling is done in fftAddAvg(), so we don't scale here
|
||||
}
|
||||
#else
|
||||
// run integer DSP FFT (takes ~x ms on ESP32, ~x ms on ESP32-S2, , ~1.5 ms on ESP32-C3) TODO: test and fill in these values
|
||||
//apply window function to samples and fill buffer with interleaved complex values [Re,Im,Re,Im,...]
|
||||
for (int i = samplesFFT - 1; i >= 0 ; i--) {
|
||||
// fill the buffer back to front to avoid overwriting samples
|
||||
int16_t windowed_sample = ((int32_t)valFFT[i] * (int32_t)windowFFT[i]) >> 15; // both values are ±15bit
|
||||
valFFT[i * 2] = windowed_sample;
|
||||
valFFT[i * 2 + 1] = 0; // set imaginary part to zero
|
||||
}
|
||||
dsps_fft2r_sc16_ansi(valFFT, samplesFFT); // perform FFT on complex value pairs (Re,Im)
|
||||
dsps_bit_rev_sc16_ansi(valFFT, samplesFFT); // bit reverse i.e. "unshuffle" the results
|
||||
valFFT[0] = 0; // set DC bin to 0, as it is not needed and can cause issues
|
||||
// convert to magnitude, FFT returns interleaved complex values [Re,Im,Re,Im,...]
|
||||
int FFT_MajorPeak_int = 0;
|
||||
int FFT_Magnitude_int = 0;
|
||||
for (int i = 1; i < samplesFFT_2; i++) { // skip [0], it is DC offset
|
||||
int32_t real_part = valFFT[i * 2];
|
||||
int32_t imag_part = valFFT[i * 2 + 1];
|
||||
valFFT[i] = sqrt32_bw(real_part * real_part + imag_part * imag_part); // note: this should never overflow as Re and Im form a vector of maximum length 32767
|
||||
if (valFFT[i] > FFT_Magnitude_int) {
|
||||
FFT_Magnitude_int = valFFT[i];
|
||||
FFT_MajorPeak_int = ((i * SAMPLE_RATE)/samplesFFT);
|
||||
}
|
||||
// note: scaling is done in fftAddAvg(), so we don't scale here
|
||||
}
|
||||
FFT_Magnitude = FFT_Magnitude_int * 512; // scale to match raw float value
|
||||
FFT_MajorPeak = FFT_MajorPeak_int;
|
||||
FFT_Magnitude = FFT_Magnitude_int;
|
||||
#endif
|
||||
#endif
|
||||
vReal[0] = 0; // The remaining DC offset on the signal produces a strong spike on position 0 that should be eliminated to avoid issues.
|
||||
|
||||
FFT.majorPeak(&FFT_MajorPeak, &FFT_Magnitude); // let the effects know which freq was most dominant
|
||||
FFT_MajorPeak = constrain(FFT_MajorPeak, 1.0f, 11025.0f); // restrict value to range expected by effects
|
||||
|
||||
#if defined(WLED_DEBUG) || defined(SR_DEBUG)
|
||||
haveDoneFFT = true;
|
||||
#endif
|
||||
} else { // noise gate closed - only clear results as FFT was skipped. MIC samples are still valid when we do this -> set all samples to 0
|
||||
memset(valFFT, 0, samplesFFT * sizeof(FFTsampleType));
|
||||
|
||||
} else { // noise gate closed - only clear results as FFT was skipped. MIC samples are still valid when we do this.
|
||||
memset(vReal, 0, samplesFFT * sizeof(float));
|
||||
FFT_MajorPeak = 1;
|
||||
FFT_Magnitude = 0.001;
|
||||
}
|
||||
|
||||
for (int i = 0; i < samplesFFT; i++) {
|
||||
float t = fabsf(vReal[i]); // just to be sure - values in fft bins should be positive any way
|
||||
vReal[i] = t / 16.0f; // Reduce magnitude. Want end result to be scaled linear and ~4096 max.
|
||||
} // for()
|
||||
|
||||
// mapping of FFT result bins to frequency channels
|
||||
if (fabsf(sampleAvg) > 0.5f) { // noise gate open
|
||||
#if 0
|
||||
@@ -511,7 +345,7 @@ void FFTcode(void * parameter)
|
||||
fftCalc[15] = fftAddAvg(194,250); // 3880 - 5000 // avoid the last 5 bins, which are usually inaccurate
|
||||
#else
|
||||
/* new mapping, optimized for 22050 Hz by softhack007 */
|
||||
// bins frequency range
|
||||
// bins frequency range
|
||||
if (useBandPassFilter) {
|
||||
// skip frequencies below 100hz
|
||||
fftCalc[ 0] = 0.8f * fftAddAvg(3,4);
|
||||
@@ -573,15 +407,12 @@ void FFTcode(void * parameter)
|
||||
// Pre / Postprocessing //
|
||||
///////////////////////////
|
||||
|
||||
static void runMicFilter(uint16_t numSamples, FFTsampleType *sampleBuffer) // pre-filtering of raw samples (band-pass)
|
||||
static void runMicFilter(uint16_t numSamples, float *sampleBuffer) // pre-filtering of raw samples (band-pass)
|
||||
{
|
||||
#if !defined(UM_AUDIOREACTIVE_USE_INTEGER_FFT)
|
||||
// low frequency cutoff parameter - see https://dsp.stackexchange.com/questions/40462/exponential-moving-average-cut-off-frequency (alpha = 2π × fc / fs)
|
||||
// low frequency cutoff parameter - see https://dsp.stackexchange.com/questions/40462/exponential-moving-average-cut-off-frequency
|
||||
//constexpr float alpha = 0.04f; // 150Hz
|
||||
//constexpr float alpha = 0.03f; // 110Hz
|
||||
//constexpr float alpha = 0.0285f; //100Hz
|
||||
constexpr float alpha = 0.0256f; //90Hz
|
||||
//constexpr float alpha = 0.0225f; // 80hz
|
||||
constexpr float alpha = 0.0225f; // 80hz
|
||||
//constexpr float alpha = 0.01693f;// 60hz
|
||||
// high frequency cutoff parameter
|
||||
//constexpr float beta1 = 0.75f; // 11Khz
|
||||
@@ -605,39 +436,6 @@ static void runMicFilter(uint16_t numSamples, FFTsampleType *sampleBuffer)
|
||||
lowfilt += alpha * (sampleBuffer[i] - lowfilt);
|
||||
sampleBuffer[i] = sampleBuffer[i] - lowfilt;
|
||||
}
|
||||
#else
|
||||
// low frequency cutoff parameter 17.15 fixed point format
|
||||
//constexpr int32_t ALPHA_FP = 1311; // 0.04f * (1<<15) (150Hz)
|
||||
//constexpr int32_t ALPHA_FP = 983; // 0.03f * (1<<15) (110Hz)
|
||||
//constexpr int32_t ALPHA_FP = 934; // 0.0285f * (1<<15) (100Hz)
|
||||
constexpr int32_t ALPHA_FP = 840; // 0.0256f * (1<<15) (90Hz)
|
||||
//constexpr int32_t ALPHA_FP = 737; // 0.0225f * (1<<15) (80Hz)
|
||||
//constexpr int32_t ALPHA_FP = 555; // 0.01693f * (1<<15) (60Hz)
|
||||
|
||||
// high frequency cutoff parameters 16.16 fixed point format
|
||||
//constexpr int32_t BETA1_FP = 49152; // 0.75f * (1<<16) (11KHz)
|
||||
//constexpr int32_t BETA1_FP = 53740; // 0.82f * (1<<16) (15KHz)
|
||||
//constexpr int32_t BETA1_FP = 54297; // 0.8285f * (1<<16) (18KHz)
|
||||
constexpr int32_t BETA1_FP = 55706; // 0.85f * (1<<16) (20KHz)
|
||||
constexpr int32_t BETA2_FP = (65536 - BETA1_FP) / 2; // ((1.0f - beta1) / 2.0f) * (1<<16)
|
||||
|
||||
static int32_t last_vals[2] = { 0 }; // FIR high freq cutoff filter (scaled by sample range)
|
||||
static int32_t lowfilt_fp = 0; // IIR low frequency cutoff filter (16.16 fixed point)
|
||||
|
||||
for (int i = 0; i < numSamples; i++) {
|
||||
// FIR lowpass filter to remove high frequency noise
|
||||
int32_t highFilteredSample_fp;
|
||||
|
||||
if (i < (numSamples - 1))
|
||||
highFilteredSample_fp = (BETA1_FP * (int32_t)sampleBuffer[i] + BETA2_FP * last_vals[0] + BETA2_FP * (int32_t)sampleBuffer[i + 1]) >> 16; // smooth out spikes
|
||||
else
|
||||
highFilteredSample_fp = (BETA1_FP * (int32_t)sampleBuffer[i] + BETA2_FP * last_vals[0] + BETA2_FP * last_vals[1]) >> 16; // special handling for last sample in array
|
||||
last_vals[1] = last_vals[0];
|
||||
last_vals[0] = (int32_t)sampleBuffer[i];
|
||||
lowfilt_fp += ALPHA_FP * (highFilteredSample_fp - (lowfilt_fp >> 15)); // low pass filter in 17.15 fixed point format
|
||||
sampleBuffer[i] = highFilteredSample_fp - (lowfilt_fp >> 15);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
static void postProcessFFTResults(bool noiseGateOpen, int numberOfChannels) // post-processing and post-amp of GEQ channels
|
||||
@@ -726,7 +524,7 @@ static void detectSamplePeak(void) {
|
||||
// Poor man's beat detection by seeing if sample > Average + some value.
|
||||
// This goes through ALL of the 255 bins - but ignores stupid settings
|
||||
// Then we got a peak, else we don't. The peak has to time out on its own in order to support UDP sound sync.
|
||||
if ((sampleAvg > 1) && (maxVol > 0) && (binNum > 4) && (valFFT[binNum] > maxVol) && ((millis() - timeOfPeak) > 100)) {
|
||||
if ((sampleAvg > 1) && (maxVol > 0) && (binNum > 4) && (vReal[binNum] > maxVol) && ((millis() - timeOfPeak) > 100)) {
|
||||
havePeak = true;
|
||||
}
|
||||
|
||||
@@ -758,8 +556,6 @@ class AudioReactive : public Usermod {
|
||||
private:
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
|
||||
static constexpr uint8_t SR_DMTYPE_NETWORK_ONLY = 254;
|
||||
|
||||
#ifndef AUDIOPIN
|
||||
int8_t audioPin = -1;
|
||||
#else
|
||||
@@ -881,9 +677,6 @@ class AudioReactive : public Usermod {
|
||||
#endif
|
||||
static const char _digitalmic[];
|
||||
static const char _addPalettes[];
|
||||
static const char _palName0[];
|
||||
static const char _palName1[];
|
||||
static const char _palName2[];
|
||||
static const char UDP_SYNC_HEADER[];
|
||||
static const char UDP_SYNC_HEADER_v1[];
|
||||
|
||||
@@ -1300,12 +1093,7 @@ class AudioReactive : public Usermod {
|
||||
|
||||
size_t packetSize = fftUdp.parsePacket();
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
if ((packetSize > 0) && ((packetSize < 5) || (packetSize > UDPSOUND_MAX_PACKET)))
|
||||
#if ESP_IDF_VERSION_MAJOR < 5
|
||||
fftUdp.flush(); // discard invalid packets (too small or too big) - only works on esp32
|
||||
#else
|
||||
fftUdp.clear(); // function was renamed in newer frameworks
|
||||
#endif
|
||||
if ((packetSize > 0) && ((packetSize < 5) || (packetSize > UDPSOUND_MAX_PACKET))) fftUdp.flush(); // discard invalid packets (too small or too big) - only works on esp32
|
||||
#endif
|
||||
if ((packetSize > 5) && (packetSize <= UDPSOUND_MAX_PACKET)) {
|
||||
//DEBUGSR_PRINTLN("Received UDP Sync Packet");
|
||||
@@ -1381,8 +1169,8 @@ class AudioReactive : public Usermod {
|
||||
periph_module_reset(PERIPH_I2S0_MODULE); // not possible on -C3
|
||||
#endif
|
||||
delay(100); // Give that poor microphone some time to setup.
|
||||
useBandPassFilter = false; // filter cuts lowest and highest frequency bands from FFT result (use on very noisy mic inputs)
|
||||
useMicFilter = true; // filter fixes aliasing to base & highest frequency bands and reduces noise floor (recommended for all mic inputs)
|
||||
|
||||
useBandPassFilter = false;
|
||||
|
||||
#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3)
|
||||
if ((i2sckPin == I2S_PIN_NO_CHANGE) && (i2ssdPin >= 0) && (i2swsPin >= 0) && ((dmType == 1) || (dmType == 4)) ) dmType = 5; // dummy user support: SCK == -1 --means--> PDM microphone
|
||||
@@ -1417,13 +1205,12 @@ class AudioReactive : public Usermod {
|
||||
case 4:
|
||||
DEBUGSR_PRINT(F("AR: Generic I2S Microphone with Master Clock - ")); DEBUGSR_PRINTLN(F(I2S_MIC_CHANNEL_TEXT));
|
||||
audioSource = new I2SSource(SAMPLE_RATE, BLOCK_SIZE, 1.0f/24.0f);
|
||||
useMicFilter = false; // I2S with Master Clock is mostly used for line-in, skip sample filtering
|
||||
delay(100);
|
||||
if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin);
|
||||
break;
|
||||
#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3)
|
||||
case 5:
|
||||
DEBUGSR_PRINT(F("AR: Generic PDM Microphone - ")); DEBUGSR_PRINTLN(F(I2S_PDM_MIC_CHANNEL_TEXT));
|
||||
DEBUGSR_PRINT(F("AR: I2S PDM Microphone - ")); DEBUGSR_PRINTLN(F(I2S_PDM_MIC_CHANNEL_TEXT));
|
||||
audioSource = new I2SSource(SAMPLE_RATE, BLOCK_SIZE, 1.0f/4.0f);
|
||||
useBandPassFilter = true; // this reduces the noise floor on SPM1423 from 5% Vpp (~380) down to 0.05% Vpp (~5)
|
||||
delay(100);
|
||||
@@ -1433,7 +1220,6 @@ class AudioReactive : public Usermod {
|
||||
case 6:
|
||||
DEBUGSR_PRINTLN(F("AR: ES8388 Source"));
|
||||
audioSource = new ES8388Source(SAMPLE_RATE, BLOCK_SIZE);
|
||||
useMicFilter = false;
|
||||
delay(100);
|
||||
if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin);
|
||||
break;
|
||||
@@ -1441,6 +1227,7 @@ class AudioReactive : public Usermod {
|
||||
#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3)
|
||||
// ADC over I2S is only possible on "classic" ESP32
|
||||
case 0:
|
||||
default:
|
||||
DEBUGSR_PRINTLN(F("AR: Analog Microphone (left channel only)."));
|
||||
audioSource = new I2SAdcSource(SAMPLE_RATE, BLOCK_SIZE);
|
||||
delay(100);
|
||||
@@ -1448,43 +1235,22 @@ class AudioReactive : public Usermod {
|
||||
if (audioSource) audioSource->initialize(audioPin);
|
||||
break;
|
||||
#endif
|
||||
|
||||
case SR_DMTYPE_NETWORK_ONLY: // dummy "network receive only" mode
|
||||
if (audioSource) delete audioSource; audioSource = nullptr;
|
||||
disableSoundProcessing = true;
|
||||
audioSyncEnabled = 2; // force udp sound receive mode
|
||||
enabled = true;
|
||||
break;
|
||||
|
||||
case 255: // 255 = -1 = no audio source
|
||||
// falls through to default
|
||||
default:
|
||||
if (audioSource) delete audioSource; audioSource = nullptr;
|
||||
disableSoundProcessing = true;
|
||||
enabled = false;
|
||||
break;
|
||||
}
|
||||
delay(250); // give microphone enough time to initialise
|
||||
|
||||
if (!audioSource && (dmType != SR_DMTYPE_NETWORK_ONLY)) enabled = false;// audio failed to initialise
|
||||
if (!audioSource) enabled = false; // audio failed to initialise
|
||||
#endif
|
||||
if (enabled) onUpdateBegin(false); // create FFT task, and initialize network
|
||||
|
||||
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
if (audioSource && FFT_Task == nullptr) enabled = false; // FFT task creation failed
|
||||
if (FFT_Task == nullptr) enabled = false; // FFT task creation failed
|
||||
if((!audioSource) || (!audioSource->isInitialized())) { // audio source failed to initialize. Still stay "enabled", as there might be input arriving via UDP Sound Sync
|
||||
#ifdef WLED_DEBUG
|
||||
#define AR_INIT_DEBUG_PRINT DEBUG_PRINTLN
|
||||
#else
|
||||
#define AR_INIT_DEBUG_PRINT DEBUGSR_PRINTLN
|
||||
#endif
|
||||
if (dmType == SR_DMTYPE_NETWORK_ONLY) {
|
||||
AR_INIT_DEBUG_PRINT(F("AR: No sound input driver configured - network receive only."));
|
||||
} else {
|
||||
AR_INIT_DEBUG_PRINT(F("AR: Failed to initialize sound input driver. Please check input PIN settings."));
|
||||
}
|
||||
#undef AR_INIT_DEBUG_PRINT
|
||||
#ifdef WLED_DEBUG
|
||||
DEBUG_PRINTLN(F("AR: Failed to initialize sound input driver. Please check input PIN settings."));
|
||||
#else
|
||||
DEBUGSR_PRINTLN(F("AR: Failed to initialize sound input driver. Please check input PIN settings."));
|
||||
#endif
|
||||
disableSoundProcessing = true;
|
||||
}
|
||||
#endif
|
||||
@@ -1538,9 +1304,14 @@ class AudioReactive : public Usermod {
|
||||
// We cannot wait indefinitely before processing audio data
|
||||
if (strip.isUpdating() && (millis() - lastUMRun < 2)) return; // be nice, but not too nice
|
||||
|
||||
// suspend local sound processing when "real time mode" is active (E131, UDP, ADALIGHT, ARTNET, DDP, DMX)
|
||||
// exception: sound input is still needed when useMainSegmentOnly - other segments are still running with local input.
|
||||
if (realtimeMode && !realtimeOverride && !useMainSegmentOnly) {
|
||||
// suspend local sound processing when "real time mode" is active (E131, UDP, ADALIGHT, ARTNET)
|
||||
if ( (realtimeOverride == REALTIME_OVERRIDE_NONE) // please add other overrides here if needed
|
||||
&&( (realtimeMode == REALTIME_MODE_GENERIC)
|
||||
||(realtimeMode == REALTIME_MODE_E131)
|
||||
||(realtimeMode == REALTIME_MODE_UDP)
|
||||
||(realtimeMode == REALTIME_MODE_ADALIGHT)
|
||||
||(realtimeMode == REALTIME_MODE_ARTNET) ) ) // please add other modes here if needed
|
||||
{
|
||||
#if defined(ARDUINO_ARCH_ESP32) && defined(WLED_DEBUG)
|
||||
if ((disableSoundProcessing == false) && (audioSyncEnabled == 0)) { // we just switched to "disabled"
|
||||
DEBUG_PRINTLN(F("[AR userLoop] realtime mode active - audio processing suspended."));
|
||||
@@ -1550,7 +1321,7 @@ class AudioReactive : public Usermod {
|
||||
disableSoundProcessing = true;
|
||||
} else {
|
||||
#if defined(ARDUINO_ARCH_ESP32) && defined(WLED_DEBUG)
|
||||
if ((disableSoundProcessing == true) && (audioSyncEnabled == 0) && audioSource && audioSource->isInitialized()) { // we just switched to "enabled"
|
||||
if ((disableSoundProcessing == true) && (audioSyncEnabled == 0) && audioSource->isInitialized()) { // we just switched to "enabled"
|
||||
DEBUG_PRINTLN(F("[AR userLoop] realtime mode ended - audio processing resumed."));
|
||||
DEBUG_PRINTF_P(PSTR(" RealtimeMode = %d; RealtimeOverride = %d\n"), int(realtimeMode), int(realtimeOverride));
|
||||
}
|
||||
@@ -1562,7 +1333,7 @@ class AudioReactive : public Usermod {
|
||||
if (audioSyncEnabled & 0x02) disableSoundProcessing = true; // make sure everything is disabled IF in audio Receive mode
|
||||
if (audioSyncEnabled & 0x01) disableSoundProcessing = false; // keep running audio IF we're in audio Transmit mode
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
if (!audioSource || !audioSource->isInitialized()) disableSoundProcessing = true; // no audio source
|
||||
if (!audioSource->isInitialized()) disableSoundProcessing = true; // no audio source
|
||||
|
||||
|
||||
// Only run the sampling code IF we're not in Receive mode or realtime mode
|
||||
@@ -1617,11 +1388,7 @@ class AudioReactive : public Usermod {
|
||||
have_new_sample = receiveAudioData();
|
||||
if (have_new_sample) last_UDPTime = millis();
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
#if ESP_IDF_VERSION_MAJOR < 5
|
||||
else fftUdp.flush(); // Flush udp input buffers if we haven't read it - avoids hickups in receive mode. Does not work on 8266.
|
||||
#else
|
||||
else fftUdp.clear(); // function was renamed in newer frameworks
|
||||
#endif
|
||||
#endif
|
||||
lastTime = millis();
|
||||
}
|
||||
@@ -1726,7 +1493,7 @@ class AudioReactive : public Usermod {
|
||||
);
|
||||
}
|
||||
micDataReal = 0.0f; // just to be sure
|
||||
if (enabled) disableSoundProcessing = false; // allows FFT_Task to run at least once, even when loop() might disable again
|
||||
if (enabled) disableSoundProcessing = false;
|
||||
updateIsRunning = init;
|
||||
}
|
||||
|
||||
@@ -1763,7 +1530,7 @@ class AudioReactive : public Usermod {
|
||||
// better would be for AudioSource to implement getType()
|
||||
if (enabled
|
||||
&& dmType == 0 && audioPin>=0
|
||||
&& (buttons[b].type == BTN_TYPE_ANALOG || buttons[b].type == BTN_TYPE_ANALOG_INVERTED)
|
||||
&& (buttonType[b] == BTN_TYPE_ANALOG || buttonType[b] == BTN_TYPE_ANALOG_INVERTED)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
@@ -1847,8 +1614,7 @@ class AudioReactive : public Usermod {
|
||||
if (audioSource->getType() == AudioSource::Type_I2SAdc) {
|
||||
infoArr.add(F("ADC analog"));
|
||||
} else {
|
||||
if (dmType == 5) infoArr.add(F("PDM digital")); // dmType 5 => generic PDM microphone
|
||||
else infoArr.add(F("I2S digital"));
|
||||
infoArr.add(F("I2S digital"));
|
||||
}
|
||||
// input level or "silence"
|
||||
if (maxSample5sec > 1.0f) {
|
||||
@@ -1966,10 +1732,14 @@ class AudioReactive : public Usermod {
|
||||
}
|
||||
#endif
|
||||
}
|
||||
if (root.containsKey(F("rmcpal")) && root[F("rmcpal")].as<bool>()) {
|
||||
// handle removal of custom palettes from JSON call so we don't break things
|
||||
removeAudioPalettes();
|
||||
}
|
||||
}
|
||||
|
||||
void onStateChange(uint8_t callMode) override {
|
||||
if (initDone && enabled && addPalettes && palettes==0) {
|
||||
if (initDone && enabled && addPalettes && palettes==0 && customPalettes.size()<10) {
|
||||
// if palettes were removed during JSON call re-add them
|
||||
createAudioPalettes();
|
||||
}
|
||||
@@ -2131,12 +1901,9 @@ class AudioReactive : public Usermod {
|
||||
uiScript.print(F("addOption(dd,'SPH0654',3);"));
|
||||
uiScript.print(F("addOption(dd,'Generic I2S with Mclk',4);"));
|
||||
#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3)
|
||||
uiScript.print(F("addOption(dd,'Generic PDM',5);"));
|
||||
uiScript.print(F("addOption(dd,'Generic I2S PDM',5);"));
|
||||
#endif
|
||||
uiScript.print(F("addOption(dd,'ES8388',6);"));
|
||||
uiScript.print(F("addOption(dd,'None - network receive only',"));
|
||||
uiScript.print(SR_DMTYPE_NETWORK_ONLY);
|
||||
uiScript.print(F(");"));
|
||||
|
||||
uiScript.print(F("dd=addDropdown(ux,'config:AGC');"));
|
||||
uiScript.print(F("addOption(dd,'Off',0);"));
|
||||
@@ -2201,21 +1968,24 @@ class AudioReactive : public Usermod {
|
||||
|
||||
void AudioReactive::removeAudioPalettes(void) {
|
||||
DEBUG_PRINTLN(F("Removing audio palettes."));
|
||||
palettes -= (int8_t)removeUsermodPalettes(_name);
|
||||
if (palettes < 0) palettes = 0; // safeguard
|
||||
while (palettes>0) {
|
||||
customPalettes.pop_back();
|
||||
DEBUG_PRINTLN(palettes);
|
||||
palettes--;
|
||||
}
|
||||
DEBUG_PRINT(F("Total # of palettes: ")); DEBUG_PRINTLN(customPalettes.size());
|
||||
}
|
||||
|
||||
void AudioReactive::createAudioPalettes(void) {
|
||||
DEBUG_PRINT(F("Total # of palettes: ")); DEBUG_PRINTLN(customPalettes.size());
|
||||
if (palettes) return;
|
||||
DEBUG_PRINTLN(F("Adding audio palettes."));
|
||||
static const char *const palNames[MAX_PALETTES] PROGMEM = {_palName0, _palName1, _palName2};
|
||||
for (int i=0; i<MAX_PALETTES; i++) {
|
||||
if (usermodPalettes.size() < WLED_MAX_USERMOD_PALETTES) {
|
||||
usermodPalettes.push_back({CRGBPalette16(CRGB(BLACK)), _name, (uint8_t)i, palNames[i]}); // start black, filled each loop by fillAudioPalettes()
|
||||
for (int i=0; i<MAX_PALETTES; i++)
|
||||
if (customPalettes.size() < 10) {
|
||||
customPalettes.push_back(CRGBPalette16(CRGB(BLACK)));
|
||||
palettes++;
|
||||
DEBUG_PRINTLN(palettes);
|
||||
} else break;
|
||||
}
|
||||
}
|
||||
|
||||
// credit @netmindz ar palette, adapted for usermod @blazoncek
|
||||
@@ -2227,12 +1997,12 @@ CRGB AudioReactive::getCRGBForBand(int x, int pal) {
|
||||
case 2:
|
||||
b = map(x, 0, 255, 0, NUM_GEQ_CHANNELS/2); // convert palette position to lower half of freq band
|
||||
hsv = CHSV(fftResult[b], 255, x);
|
||||
value = hsv; // convert to R,G,B
|
||||
hsv2rgb_rainbow(hsv, value); // convert to R,G,B
|
||||
break;
|
||||
case 1:
|
||||
b = map(x, 1, 255, 0, 10); // convert palette position to lower half of freq band
|
||||
hsv = CHSV(fftResult[b], 255, map(fftResult[b], 0, 255, 30, 255)); // pick hue
|
||||
value = hsv; // convert to R,G,B
|
||||
hsv2rgb_rainbow(hsv, value); // convert to R,G,B
|
||||
break;
|
||||
default:
|
||||
if (x == 1) {
|
||||
@@ -2249,10 +2019,9 @@ CRGB AudioReactive::getCRGBForBand(int x, int pal) {
|
||||
|
||||
void AudioReactive::fillAudioPalettes() {
|
||||
if (!palettes) return;
|
||||
// Scan by name pointer identity to find the palettes we added, palIndex = 0/1/2... selects the getCRGBForBand variant, matching how the entries were created.
|
||||
for (auto &ump : usermodPalettes) {
|
||||
if (ump.name != _name) continue;
|
||||
const int pal = ump.palIndex;
|
||||
size_t lastCustPalette = customPalettes.size();
|
||||
if (int(lastCustPalette) >= palettes) lastCustPalette -= palettes;
|
||||
for (int pal=0; pal<palettes; pal++) {
|
||||
uint8_t tcp[16]; // Needs to be 4 times however many colors are being used.
|
||||
// 3 colors = 12, 4 colors = 16, etc.
|
||||
|
||||
@@ -2260,26 +2029,26 @@ void AudioReactive::fillAudioPalettes() {
|
||||
tcp[1] = 0;
|
||||
tcp[2] = 0;
|
||||
tcp[3] = 0;
|
||||
|
||||
|
||||
CRGB rgb = getCRGBForBand(1, pal);
|
||||
tcp[4] = 1; // anchor of first color
|
||||
tcp[5] = rgb.r;
|
||||
tcp[6] = rgb.g;
|
||||
tcp[7] = rgb.b;
|
||||
|
||||
|
||||
rgb = getCRGBForBand(128, pal);
|
||||
tcp[8] = 128;
|
||||
tcp[9] = rgb.r;
|
||||
tcp[10] = rgb.g;
|
||||
tcp[11] = rgb.b;
|
||||
|
||||
|
||||
rgb = getCRGBForBand(255, pal);
|
||||
tcp[12] = 255; // anchor of last color - must be 255
|
||||
tcp[13] = rgb.r;
|
||||
tcp[14] = rgb.g;
|
||||
tcp[15] = rgb.b;
|
||||
|
||||
ump.palette.loadDynamicGradientPalette(tcp);
|
||||
customPalettes[lastCustPalette+pal].loadDynamicGradientPalette(tcp);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2294,10 +2063,7 @@ const char AudioReactive::_inputLvl[] PROGMEM = "inputLevel";
|
||||
const char AudioReactive::_analogmic[] PROGMEM = "analogmic";
|
||||
#endif
|
||||
const char AudioReactive::_digitalmic[] PROGMEM = "digitalmic";
|
||||
const char AudioReactive::_addPalettes[] PROGMEM = "add-palettes";
|
||||
const char AudioReactive::_palName0[] PROGMEM = "Ratio";
|
||||
const char AudioReactive::_palName1[] PROGMEM = "Hue";
|
||||
const char AudioReactive::_palName2[] PROGMEM = "Spectrum";
|
||||
const char AudioReactive::_addPalettes[] PROGMEM = "add-palettes";
|
||||
const char AudioReactive::UDP_SYNC_HEADER[] PROGMEM = "00002"; // new sync header version, as format no longer compatible with previous structure
|
||||
const char AudioReactive::UDP_SYNC_HEADER_v1[] PROGMEM = "00001"; // old sync header version - need to add backwards-compatibility feature
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
// see https://docs.espressif.com/projects/esp-idf/en/latest/esp32s3/hw-reference/chip-series-comparison.html#related-documents
|
||||
// and https://docs.espressif.com/projects/esp-idf/en/latest/esp32s3/api-reference/peripherals/i2s.html#overview-of-all-modes
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32C2) || defined(CONFIG_IDF_TARGET_ESP32C5) || defined(CONFIG_IDF_TARGET_ESP32C6) || defined(CONFIG_IDF_TARGET_ESP32H2) || defined(ESP8266) || defined(ESP8265)
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32C2) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32C5) || defined(CONFIG_IDF_TARGET_ESP32C6) || defined(CONFIG_IDF_TARGET_ESP32H2) || defined(ESP8266) || defined(ESP8265)
|
||||
// there are two things in these MCUs that could lead to problems with audio processing:
|
||||
// * no floating point hardware (FPU) support - FFT uses float calculations. If done in software, a strong slow-down can be expected (between 8x and 20x)
|
||||
// * single core, so FFT task might slow down other things like LED updates
|
||||
@@ -71,7 +71,7 @@
|
||||
* if you want to receive two channels, one is the actual data from microphone and another channel is suppose to receive 0, it's different data in two channels, you need to choose I2S_CHANNEL_FMT_RIGHT_LEFT in this case.
|
||||
*/
|
||||
|
||||
#if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0)) && (ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(4, 5, 0))
|
||||
#if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0)) && (ESP_IDF_VERSION <= ESP_IDF_VERSION_VAL(4, 4, 6))
|
||||
// espressif bug: only_left has no sound, left and right are swapped
|
||||
// https://github.com/espressif/esp-idf/issues/9635 I2S mic not working since 4.4 (IDFGH-8138)
|
||||
// https://github.com/espressif/esp-idf/issues/8538 I2S channel selection issue? (IDFGH-6918)
|
||||
@@ -134,7 +134,7 @@ class AudioSource {
|
||||
Read num_samples from the microphone, and store them in the provided
|
||||
buffer
|
||||
*/
|
||||
virtual void getSamples(FFTsampleType *buffer, uint16_t num_samples) = 0;
|
||||
virtual void getSamples(float *buffer, uint16_t num_samples) = 0;
|
||||
|
||||
/* check if the audio source driver was initialized successfully */
|
||||
virtual bool isInitialized(void) {return(_initialized);}
|
||||
@@ -225,17 +225,15 @@ class I2SSource : public AudioSource {
|
||||
|
||||
_config.mode = i2s_mode_t(I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_PDM); // Change mode to pdm if clock pin not provided. PDM is not supported on ESP32-S2. PDM RX not supported on ESP32-C3
|
||||
_config.channel_format =I2S_PDM_MIC_CHANNEL; // seems that PDM mono mode always uses left channel.
|
||||
_config.use_apll = false; // don't use aPLL clock source (fix for #5391)
|
||||
_config.use_apll = true; // experimental - use aPLL clock source to improve sampling quality
|
||||
#endif
|
||||
}
|
||||
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 2, 0)
|
||||
if (mclkPin != I2S_PIN_NO_CHANGE) {
|
||||
#if !defined(WLED_USE_ETHERNET) // fix for #5391 aPLL resource conflict - aPLL is needed for ethernet boards with internal RMII clock
|
||||
_config.use_apll = true; // experimental - use aPLL clock source to improve sampling quality, and to avoid glitches.
|
||||
// //_config.fixed_mclk = 512 * _sampleRate;
|
||||
// //_config.fixed_mclk = 256 * _sampleRate;
|
||||
#endif
|
||||
}
|
||||
|
||||
#if !defined(SOC_I2S_SUPPORTS_APLL)
|
||||
@@ -316,7 +314,7 @@ class I2SSource : public AudioSource {
|
||||
if (_mclkPin != I2S_PIN_NO_CHANGE) PinManager::deallocatePin(_mclkPin, PinOwner::UM_Audioreactive);
|
||||
}
|
||||
|
||||
virtual void getSamples(FFTsampleType *buffer, uint16_t num_samples) {
|
||||
virtual void getSamples(float *buffer, uint16_t num_samples) {
|
||||
if (_initialized) {
|
||||
esp_err_t err;
|
||||
size_t bytes_read = 0; /* Counter variable to check if we actually got enough data */
|
||||
@@ -334,36 +332,19 @@ class I2SSource : public AudioSource {
|
||||
return;
|
||||
}
|
||||
|
||||
// Store samples in sample buffer
|
||||
#if defined(UM_AUDIOREACTIVE_USE_INTEGER_FFT)
|
||||
//constexpr int32_t FIXEDSHIFT = 8; // shift by 8 bits for fixed point math (no loss at 24bit input sample resolution)
|
||||
//int32_t intSampleScale = _sampleScale * (1<<FIXEDSHIFT); // _sampleScale <= 1.0f, shift for fixed point math
|
||||
#endif
|
||||
|
||||
// Store samples in sample buffer and update DC offset
|
||||
for (int i = 0; i < num_samples; i++) {
|
||||
newSamples[i] = postProcessSample(newSamples[i]); // perform postprocessing (needed for ADC samples)
|
||||
|
||||
#if !defined(UM_AUDIOREACTIVE_USE_INTEGER_FFT)
|
||||
#ifdef I2S_SAMPLE_DOWNSCALE_TO_16BIT
|
||||
float currSample = (float) newSamples[i] / 65536.0f; // 32bit input -> 16bit; keeping lower 16bits as decimal places
|
||||
#else
|
||||
float currSample = (float) newSamples[i]; // 16bit input -> use as-is
|
||||
#endif
|
||||
buffer[i] = currSample;
|
||||
buffer[i] *= _sampleScale; // scale samples
|
||||
newSamples[i] = postProcessSample(newSamples[i]); // perform postprocessing (needed for ADC samples)
|
||||
|
||||
float currSample = 0.0f;
|
||||
#ifdef I2S_SAMPLE_DOWNSCALE_TO_16BIT
|
||||
currSample = (float) newSamples[i] / 65536.0f; // 32bit input -> 16bit; keeping lower 16bits as decimal places
|
||||
#else
|
||||
#ifdef I2S_SAMPLE_DOWNSCALE_TO_16BIT
|
||||
// note on sample scaling: scaling is only used for inputs with master clock and those are better suited for ESP32 or S3
|
||||
// execution speed is critical on single core MCUs
|
||||
//int32_t currSample = newSamples[i] >> FIXEDSHIFT; // shift to avoid overlow in multiplication
|
||||
//currSample = (currSample * intSampleScale) >> 16; // scale samples, shift down to 16bit
|
||||
int16_t currSample = newSamples[i] >> 16; // no sample scaling, just shift down to 16bit (not scaling saves ~0.4ms on C3)
|
||||
#else
|
||||
//int32_t currSample = (newSamples[i] * intSampleScale) >> FIXEDSHIFT; // scale samples, shift back down to 16bit
|
||||
int16_t currSample = newSamples[i]; // 16bit input -> use as-is
|
||||
#endif
|
||||
buffer[i] = (int16_t)currSample;
|
||||
currSample = (float) newSamples[i]; // 16bit input -> use as-is
|
||||
#endif
|
||||
buffer[i] = currSample;
|
||||
buffer[i] *= _sampleScale; // scale samples
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -641,7 +622,7 @@ class I2SAdcSource : public I2SSource {
|
||||
}
|
||||
|
||||
// see example in https://github.com/espressif/arduino-esp32/blob/master/libraries/ESP32/examples/I2S/HiFreq_ADC/HiFreq_ADC.ino
|
||||
adc1_config_channel_atten(adc1_channel_t(channel), ADC_ATTEN_DB_12); // configure ADC input amplification
|
||||
adc1_config_channel_atten(adc1_channel_t(channel), ADC_ATTEN_DB_11); // configure ADC input amplification
|
||||
|
||||
#if defined(I2S_GRAB_ADC1_COMPLETELY)
|
||||
// according to docs from espressif, the ADC needs to be started explicitly
|
||||
@@ -706,7 +687,7 @@ class I2SAdcSource : public I2SSource {
|
||||
}
|
||||
|
||||
|
||||
void getSamples(FFTsampleType *buffer, uint16_t num_samples) {
|
||||
void getSamples(float *buffer, uint16_t num_samples) {
|
||||
/* Enable ADC. This has to be enabled and disabled directly before and
|
||||
* after sampling, otherwise Wifi dies
|
||||
*/
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
#include "wled.h"
|
||||
#include "driver/rtc_io.h"
|
||||
#ifndef CONFIG_IDF_TARGET_ESP32C3
|
||||
#include "soc/touch_sensor_periph.h"
|
||||
#endif
|
||||
|
||||
#ifdef ESP8266
|
||||
#error The "Deep Sleep" usermod does not support ESP8266
|
||||
#endif
|
||||
@@ -23,34 +21,27 @@
|
||||
#define DEEPSLEEP_DELAY 1
|
||||
#endif
|
||||
|
||||
#ifndef DEEPSLEEP_WAKEUP_TOUCH_PIN
|
||||
#define DEEPSLEEP_WAKEUP_TOUCH_PIN 1
|
||||
#endif
|
||||
RTC_DATA_ATTR bool powerup = true; // this is first boot after power cycle. note: variable in RTC data persists on a reboot
|
||||
RTC_DATA_ATTR uint8_t wakeupPreset = 0; // preset to apply after deep sleep wakeup (0 = none), set to timer macro preset
|
||||
RTC_DATA_ATTR bool powerup = true; // variable in RTC data persists on a reboot
|
||||
|
||||
class DeepSleepUsermod : public Usermod {
|
||||
|
||||
private:
|
||||
bool enabled = false; // do not enable by default
|
||||
|
||||
bool enabled = true;
|
||||
bool initDone = false;
|
||||
uint8_t wakeupPin = DEEPSLEEP_WAKEUPPIN;
|
||||
uint8_t wakeWhenHigh = DEEPSLEEP_WAKEWHENHIGH; // wake up when pin goes high if 1, triggers on low if 0
|
||||
bool noPull = true; // use pullup/pulldown resistor
|
||||
bool enableTouchWakeup = false;
|
||||
uint8_t touchPin = DEEPSLEEP_WAKEUP_TOUCH_PIN;
|
||||
int wakeupAfter = DEEPSLEEP_WAKEUPINTERVAL; // in seconds, <=0: button only
|
||||
bool presetWake = true; // wakeup timer for preset
|
||||
int sleepDelay = DEEPSLEEP_DELAY; // in seconds, 0 = immediate
|
||||
int delaycounter = 10; // delay deep sleep at bootup until preset settings are applied, force wake up if offmode persists after bootup
|
||||
int delaycounter = 5; // delay deep sleep at bootup until preset settings are applied
|
||||
uint32_t lastLoopTime = 0;
|
||||
|
||||
// string that are used multiple time (this will save some flash memory)
|
||||
static const char _name[];
|
||||
static const char _enabled[];
|
||||
|
||||
bool pin_is_valid(uint8_t wakePin) {
|
||||
#ifdef CONFIG_IDF_TARGET_ESP32 //ESP32: GPIOs 0,2,4, 12-15, 25-39 can be used for wake-up. note: input-only GPIOs 34-39 do not have internal pull resistors
|
||||
#ifdef CONFIG_IDF_TARGET_ESP32 //ESP32: GPIOs 0,2,4, 12-15, 25-39 can be used for wake-up
|
||||
if (wakePin == 0 || wakePin == 2 || wakePin == 4 || (wakePin >= 12 && wakePin <= 15) || (wakePin >= 25 && wakePin <= 27) || (wakePin >= 32 && wakePin <= 39)) {
|
||||
return true;
|
||||
}
|
||||
@@ -69,57 +60,8 @@ class DeepSleepUsermod : public Usermod {
|
||||
return false;
|
||||
}
|
||||
|
||||
// functions to calculate time difference between now and next scheduled timer event
|
||||
int calculateTimeDifference(int hour1, int minute1, int hour2, int minute2) {
|
||||
int totalMinutes1 = hour1 * 60 + minute1;
|
||||
int totalMinutes2 = hour2 * 60 + minute2;
|
||||
if (totalMinutes2 < totalMinutes1) {
|
||||
totalMinutes2 += 24 * 60;
|
||||
}
|
||||
return totalMinutes2 - totalMinutes1;
|
||||
}
|
||||
|
||||
int findNextTimerInterval() {
|
||||
if (toki.getTimeSource() == TOKI_TS_NONE) {
|
||||
DEBUG_PRINTLN("DeepSleep: local time not yet synchronized, skipping timer check.");
|
||||
return -1;
|
||||
}
|
||||
int currentHour = hour(localTime);
|
||||
int currentMinute = minute(localTime);
|
||||
int currentWeekday = weekdayMondayFirst(); // 1=Monday ... 7=Sunday
|
||||
int minDifference = INT_MAX;
|
||||
|
||||
for (size_t i = 0; i < timers.size(); i++) {
|
||||
const Timer& t = timers[i];
|
||||
// only regular enabled timers with valid date range can be used for wake scheduling
|
||||
if (!t.isEnabled() || !t.isRegular()) continue;
|
||||
if (!isTodayInDateRange(t.monthStart, t.dayStart, t.monthEnd, t.dayEnd)) continue;
|
||||
|
||||
// check all weekdays for the current timer, starting from today
|
||||
for (int dayOffset = 0; dayOffset < 7; dayOffset++) {
|
||||
int checkWeekday = (currentWeekday + dayOffset) % 7; // 1-7, check all weekdays starting from today
|
||||
if (checkWeekday == 0) {
|
||||
checkWeekday = 7; // sunday is 7 not 0
|
||||
}
|
||||
|
||||
int targetHour = t.hour;
|
||||
int targetMinute = t.minute;
|
||||
if ((t.weekdays >> checkWeekday) & 0x01) {
|
||||
if (dayOffset == 0 && (targetHour < currentHour || (targetHour == currentHour && targetMinute <= currentMinute)))
|
||||
continue; // skip if time has already passed today
|
||||
|
||||
int timeDifference = calculateTimeDifference(currentHour, currentMinute, targetHour + (dayOffset * 24), targetMinute);
|
||||
if (timeDifference < minDifference) {
|
||||
minDifference = timeDifference;
|
||||
wakeupPreset = t.preset;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return minDifference;
|
||||
}
|
||||
|
||||
public:
|
||||
|
||||
inline void enable(bool enable) { enabled = enable; } // Enable/Disable the usermod
|
||||
inline bool isEnabled() { return enabled; } //Get usermod enabled/disabled state
|
||||
|
||||
@@ -127,40 +69,26 @@ class DeepSleepUsermod : public Usermod {
|
||||
void setup() {
|
||||
//TODO: if the de-init of RTC pins is required to do it could be done here
|
||||
//rtc_gpio_deinit(wakeupPin);
|
||||
#ifdef WLED_DEBUG
|
||||
DEBUG_PRINTF("sleep wakeup cause: %d\n", esp_sleep_get_wakeup_cause());
|
||||
#endif
|
||||
if (esp_sleep_get_wakeup_cause() != ESP_SLEEP_WAKEUP_TIMER)
|
||||
wakeupPreset = 0; // not a timed wakeup, don't apply preset
|
||||
initDone = true;
|
||||
}
|
||||
|
||||
void loop() {
|
||||
if (!enabled) return;
|
||||
if (!offMode) { // LEDs are on
|
||||
if (!enabled || !offMode) { // disabled or LEDs are on
|
||||
lastLoopTime = 0; // reset timer
|
||||
if (delaycounter)
|
||||
delaycounter--; // decrease delay counter if LEDs are on (they are always turned on after a wake-up, see below)
|
||||
else if (wakeupPreset)
|
||||
applyPreset(wakeupPreset); // apply preset if set, this ensures macro is applied even if we missed the wake-up time
|
||||
return;
|
||||
}
|
||||
|
||||
if (sleepDelay > 0) {
|
||||
powerup = false; // disable "safety" powerup sleep if delay is set
|
||||
if (lastLoopTime == 0)
|
||||
lastLoopTime = millis(); // initialize
|
||||
if (millis() - lastLoopTime < sleepDelay * 1000)
|
||||
return; // wait until delay is over
|
||||
} else if (powerup && delaycounter) {
|
||||
delaycounter--; // on first boot without sleepDelay set, do not force-turn on
|
||||
delay(1000); // just in case: give user a short ~10s window to turn LEDs on in UI (delaycounter is 10 by default)
|
||||
return;
|
||||
if(lastLoopTime == 0) lastLoopTime = millis(); // initialize
|
||||
if (millis() - lastLoopTime < sleepDelay * 1000) {
|
||||
return; // wait until delay is over
|
||||
}
|
||||
}
|
||||
if (powerup == false && delaycounter) { // delay sleep in case a preset is being loaded and turnOnAtBoot is disabled (beginStrip() / handleIO() does enable offMode temporarily in this case)
|
||||
|
||||
if(powerup == false && delaycounter) { // delay sleep in case a preset is being loaded and turnOnAtBoot is disabled (handleIO() does enable offMode temporarily in this case)
|
||||
delaycounter--;
|
||||
if (delaycounter == 1 && offMode) { // force turn on, no matter the settings (device is bricked if user set sleepDelay=0, no bootup preset and turnOnAtBoot=false)
|
||||
if (briS == 0) bri = 10; // turn on and set low brightness to avoid automatic turn off
|
||||
if(delaycounter == 2 && offMode) { // force turn on, no matter the settings (device is bricked if user set sleepDelay=0, no bootup preset and turnOnAtBoot=false)
|
||||
if (briS == 0) bri = 10; // turn on at low brightness
|
||||
else bri = briS;
|
||||
strip.setBrightness(bri); // needed to make handleIO() not turn off LEDs (really? does not help in bootup preset)
|
||||
offMode = false;
|
||||
@@ -171,81 +99,57 @@ class DeepSleepUsermod : public Usermod {
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
DEBUG_PRINTLN(F("DeepSleep UM: entering deep sleep..."));
|
||||
powerup = false; // turn leds on in all subsequent bootups (overrides Turn LEDs on after power up/reset' at reboot)
|
||||
if (!pin_is_valid(wakeupPin)) return;
|
||||
if(!pin_is_valid(wakeupPin)) return;
|
||||
esp_err_t halerror = ESP_OK;
|
||||
pinMode(wakeupPin, INPUT); // make sure GPIO is input with pullup/pulldown disabled
|
||||
esp_sleep_disable_wakeup_source(ESP_SLEEP_WAKEUP_ALL); //disable all wake-up sources (just in case)
|
||||
|
||||
uint32_t wakeupAfterSec = 0;
|
||||
if (presetWake) {
|
||||
int nextInterval = findNextTimerInterval();
|
||||
if (nextInterval > 1 && nextInterval < INT_MAX)
|
||||
wakeupAfterSec = (nextInterval - 1) * 60; // wakeup before next preset
|
||||
}
|
||||
if (wakeupAfter > 0) { // user-defined interval
|
||||
if (wakeupAfterSec == 0 || (uint32_t)wakeupAfter < wakeupAfterSec) {
|
||||
wakeupAfterSec = wakeupAfter;
|
||||
}
|
||||
}
|
||||
if (wakeupAfterSec > 0) {
|
||||
esp_sleep_enable_timer_wakeup(wakeupAfterSec * (uint64_t)1e6);
|
||||
DEBUG_PRINTF("wakeup after %d seconds\n", wakeupAfterSec);
|
||||
}
|
||||
if(wakeupAfter)
|
||||
esp_sleep_enable_timer_wakeup((uint64_t)wakeupAfter * (uint64_t)1e6); //sleep for x seconds
|
||||
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32C3) // ESP32 C3
|
||||
gpio_hold_dis((gpio_num_t)wakeupPin); // disable hold and configure pin
|
||||
if (wakeWhenHigh)
|
||||
halerror = esp_deep_sleep_enable_gpio_wakeup(1<<wakeupPin, ESP_GPIO_WAKEUP_GPIO_HIGH);
|
||||
if(noPull)
|
||||
gpio_sleep_set_pull_mode((gpio_num_t)wakeupPin, GPIO_FLOATING);
|
||||
else { // enable pullup/pulldown resistor
|
||||
if(wakeWhenHigh)
|
||||
gpio_sleep_set_pull_mode((gpio_num_t)wakeupPin, GPIO_PULLDOWN_ONLY);
|
||||
else
|
||||
halerror = esp_deep_sleep_enable_gpio_wakeup(1<<wakeupPin, ESP_GPIO_WAKEUP_GPIO_LOW);
|
||||
// note: on C3 calling esp_deep_sleep_enable_gpio_wakeup() automatically enables pullup/pulldown unless we call gpio_hold_en() which overrides that
|
||||
gpio_pullup_dis((gpio_num_t)wakeupPin); // disable pull resistors by default
|
||||
gpio_pulldown_dis((gpio_num_t)wakeupPin);
|
||||
if (!noPull) {
|
||||
if (wakeWhenHigh) {
|
||||
gpio_pulldown_en((gpio_num_t)wakeupPin);
|
||||
} else {
|
||||
gpio_pullup_en((gpio_num_t)wakeupPin);
|
||||
}
|
||||
}
|
||||
gpio_hold_en((gpio_num_t)wakeupPin); // hold the configured GPIO state during deep sleep, overrides the automatic pullup/pulldown, see note above
|
||||
gpio_sleep_set_pull_mode((gpio_num_t)wakeupPin, GPIO_PULLUP_ONLY);
|
||||
}
|
||||
if(wakeWhenHigh)
|
||||
halerror = esp_deep_sleep_enable_gpio_wakeup(1<<wakeupPin, ESP_GPIO_WAKEUP_GPIO_HIGH);
|
||||
else
|
||||
halerror = esp_deep_sleep_enable_gpio_wakeup(1<<wakeupPin, ESP_GPIO_WAKEUP_GPIO_LOW);
|
||||
#else // ESP32, S2, S3
|
||||
rtc_gpio_hold_dis((gpio_num_t)wakeupPin); // disable hold so we can (re)configure pin
|
||||
rtc_gpio_init((gpio_num_t)wakeupPin); // hand the pin over to RTC module
|
||||
rtc_gpio_set_direction((gpio_num_t)wakeupPin, RTC_GPIO_MODE_INPUT_ONLY);
|
||||
rtc_gpio_pullup_dis((gpio_num_t)wakeupPin); // disable pull resistors by default
|
||||
gpio_pulldown_dis((gpio_num_t)wakeupPin); // disable internal pull resistors for GPIO use
|
||||
gpio_pullup_dis((gpio_num_t)wakeupPin);
|
||||
if(noPull) {
|
||||
rtc_gpio_pullup_dis((gpio_num_t)wakeupPin);
|
||||
rtc_gpio_pulldown_dis((gpio_num_t)wakeupPin);
|
||||
if (!noPull) {
|
||||
if (wakeWhenHigh)
|
||||
rtc_gpio_pulldown_en((gpio_num_t)wakeupPin);
|
||||
else
|
||||
rtc_gpio_pullup_en((gpio_num_t)wakeupPin);
|
||||
}
|
||||
if (wakeWhenHigh)
|
||||
halerror = esp_sleep_enable_ext1_wakeup(1ULL << wakeupPin, ESP_EXT1_WAKEUP_ANY_HIGH); // use ext1 as ext0 does not work with touch wakeup
|
||||
}
|
||||
else { // enable pullup/pulldown resistor for RTC use
|
||||
if(wakeWhenHigh)
|
||||
rtc_gpio_pulldown_en((gpio_num_t)wakeupPin);
|
||||
else
|
||||
halerror = esp_sleep_enable_ext1_wakeup(1ULL << wakeupPin, ESP_EXT1_WAKEUP_ALL_LOW);
|
||||
rtc_gpio_pullup_en((gpio_num_t)wakeupPin);
|
||||
}
|
||||
if(wakeWhenHigh)
|
||||
halerror = esp_sleep_enable_ext0_wakeup((gpio_num_t)wakeupPin, HIGH); // only RTC pins can be used
|
||||
else
|
||||
halerror = esp_sleep_enable_ext0_wakeup((gpio_num_t)wakeupPin, LOW);
|
||||
#endif
|
||||
|
||||
if (enableTouchWakeup) {
|
||||
#ifdef SOC_TOUCH_VERSION_2 // S2 and S3 use much higher thresholds, see notes in pin_manager
|
||||
touchSleepWakeUpEnable(touchPin, touchThreshold << 4); // ESP32 S2 & S3: lower threshold = more sensitive
|
||||
#else
|
||||
touchSleepWakeUpEnable(touchPin, touchThreshold); // ESP32: use normal threshold (higher = more sensitive)
|
||||
#endif
|
||||
}
|
||||
delay(1); // wait for pins to be ready
|
||||
rtc_gpio_hold_en((gpio_num_t)wakeupPin); // latch and hold the configured GPIO state during deep sleep
|
||||
#endif
|
||||
WiFi.mode(WIFI_OFF); // Completely shut down the Wi-Fi module
|
||||
if (halerror == ESP_OK) esp_deep_sleep_start(); // go into deep sleep
|
||||
delay(1); // wait for pin to be ready
|
||||
if(halerror == ESP_OK) esp_deep_sleep_start(); // go into deep sleep
|
||||
else DEBUG_PRINTLN(F("sleep failed"));
|
||||
}
|
||||
|
||||
//void connected() {} //unused, this is called every time the WiFi is (re)connected
|
||||
|
||||
void addToConfig(JsonObject& root) override
|
||||
void addToConfig(JsonObject& root) override
|
||||
{
|
||||
JsonObject top = root.createNestedObject(FPSTR(_name));
|
||||
top[FPSTR(_enabled)] = enabled;
|
||||
@@ -253,11 +157,6 @@ class DeepSleepUsermod : public Usermod {
|
||||
top["gpio"] = wakeupPin;
|
||||
top["wakeWhen"] = wakeWhenHigh;
|
||||
top["pull"] = noPull;
|
||||
#ifndef CONFIG_IDF_TARGET_ESP32C3
|
||||
top["enableTouchWakeup"] = enableTouchWakeup;
|
||||
top["touchPin"] = touchPin;
|
||||
#endif
|
||||
top["presetWake"] = presetWake;
|
||||
top["wakeAfter"] = wakeupAfter;
|
||||
top["delaySleep"] = sleepDelay;
|
||||
}
|
||||
@@ -277,11 +176,6 @@ class DeepSleepUsermod : public Usermod {
|
||||
}
|
||||
configComplete &= getJsonValue(top["wakeWhen"], wakeWhenHigh, DEEPSLEEP_WAKEWHENHIGH); // default to wake on low
|
||||
configComplete &= getJsonValue(top["pull"], noPull, DEEPSLEEP_DISABLEPULL); // default to no pullup/pulldown
|
||||
#ifndef CONFIG_IDF_TARGET_ESP32C3
|
||||
configComplete &= getJsonValue(top["enableTouchWakeup"], enableTouchWakeup);
|
||||
configComplete &= getJsonValue(top["touchPin"], touchPin, DEEPSLEEP_WAKEUP_TOUCH_PIN);
|
||||
#endif
|
||||
configComplete &= getJsonValue(top["presetWake"], presetWake);
|
||||
configComplete &= getJsonValue(top["wakeAfter"], wakeupAfter, DEEPSLEEP_WAKEUPINTERVAL);
|
||||
configComplete &= getJsonValue(top["delaySleep"], sleepDelay, DEEPSLEEP_DELAY);
|
||||
|
||||
@@ -306,19 +200,6 @@ class DeepSleepUsermod : public Usermod {
|
||||
oappend(SET_F(");"));
|
||||
}
|
||||
}
|
||||
#ifndef CONFIG_IDF_TARGET_ESP32C3
|
||||
// dropdown for touch wakeupPin
|
||||
oappend(SET_F("dd=addDropdown('DeepSleep','touchPin');"));
|
||||
for (int touchchannel = 0; touchchannel < SOC_TOUCH_SENSOR_NUM; touchchannel++) {
|
||||
if (touch_sensor_channel_io_map[touchchannel] >= 0) {
|
||||
oappend(SET_F("addOption(dd,'"));
|
||||
oappend(String(touch_sensor_channel_io_map[touchchannel]).c_str());
|
||||
oappend(SET_F("',"));
|
||||
oappend(String(touch_sensor_channel_io_map[touchchannel]).c_str());
|
||||
oappend(SET_F(");"));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
oappend(SET_F("dd=addDropdown('DeepSleep','wakeWhen');"));
|
||||
oappend(SET_F("addOption(dd,'Low',0);"));
|
||||
@@ -326,7 +207,6 @@ class DeepSleepUsermod : public Usermod {
|
||||
|
||||
oappend(SET_F("addInfo('DeepSleep:pull',1,'','-up/down disable: ');")); // first string is suffix, second string is prefix
|
||||
oappend(SET_F("addInfo('DeepSleep:wakeAfter',1,'seconds <i>(0 = never)<i>');"));
|
||||
oappend(SET_F("addInfo('DeepSleep:presetWake',1,'<i>(wake up before next preset timer)<i>');"));
|
||||
oappend(SET_F("addInfo('DeepSleep:delaySleep',1,'seconds <i>(0 = sleep at powerup)<i>');")); // first string is suffix, second string is prefix
|
||||
}
|
||||
|
||||
@@ -337,6 +217,7 @@ class DeepSleepUsermod : public Usermod {
|
||||
uint16_t getId() {
|
||||
return USERMOD_ID_DEEP_SLEEP;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
// add more strings here to reduce flash memory usage
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Deep Sleep usermod
|
||||
|
||||
This usermod unleashes the low power capabilities of th ESP: when you power off your LEDs (using the UI power button or a macro) the ESP will be put into deep sleep mode, reducing power consumption to a minimum.
|
||||
During deep sleep the ESP is shut down completely: no WiFi, no CPU, no outputs. The only way to wake it up is by using an external signal, a button, or an automation timer set to the nearest preset time. Once it wakes from deep sleep it reboots so ***make sure to use a boot-up preset.***
|
||||
During deep sleep the ESP is shut down completely: no WiFi, no CPU, no outputs. The only way to wake it up is to use an external signal or a button. Once it wakes from deep sleep it reboots so ***make sure to use a boot-up preset.***
|
||||
|
||||
# A word of warning
|
||||
|
||||
@@ -28,7 +28,7 @@ For lowest power consumption, remove the Power LED and make sure your board does
|
||||
|
||||
The GPIOs that can be used to wake the ESP from deep sleep are limited. Only pins connected to the internal RTC unit can be used:
|
||||
|
||||
- ESP32: GPIO 0, 2, 4, 12-15, 25-39 note: input-only GPIOs 34-39 do not have internal pull up/down resistors, external resistors are required
|
||||
- ESP32: GPIO 0, 2, 4, 12-15, 25-39
|
||||
- ESP32 S3: GPIO 0-21
|
||||
- ESP32 S2: GPIO 0-21
|
||||
- ESP32 C3: GPIO 0-5
|
||||
@@ -61,7 +61,6 @@ To override the default settings, place the `#define` in wled.h or add `-D DEEPS
|
||||
* `DEEPSLEEP_DISABLEPULL` - if defined, internal pullup/pulldown is disabled in deep sleep (default is ebnabled)
|
||||
* `DEEPSLEEP_WAKEUPINTERVAL` - number of seconds after which a wake-up happens automatically, sooner if button is pressed. 0 = never. accuracy is about 2%
|
||||
* `DEEPSLEEP_DELAY` - delay between power-off and sleep
|
||||
* `DEEPSLEEP_WAKEUP_TOUCH_PIN` - specify GPIO pin used for touch-based wakeup
|
||||
|
||||
example for env build flags:
|
||||
`-D USERMOD_DEEP_SLEEP`
|
||||
|
||||
@@ -562,11 +562,11 @@ void MultiRelay::loop() {
|
||||
bool MultiRelay::handleButton(uint8_t b) {
|
||||
yield();
|
||||
if (!enabled
|
||||
|| buttons[b].type == BTN_TYPE_NONE
|
||||
|| buttons[b].type == BTN_TYPE_RESERVED
|
||||
|| buttons[b].type == BTN_TYPE_PIR_SENSOR
|
||||
|| buttons[b].type == BTN_TYPE_ANALOG
|
||||
|| buttons[b].type == BTN_TYPE_ANALOG_INVERTED) {
|
||||
|| buttonType[b] == BTN_TYPE_NONE
|
||||
|| buttonType[b] == BTN_TYPE_RESERVED
|
||||
|| buttonType[b] == BTN_TYPE_PIR_SENSOR
|
||||
|| buttonType[b] == BTN_TYPE_ANALOG
|
||||
|| buttonType[b] == BTN_TYPE_ANALOG_INVERTED) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -581,20 +581,20 @@ bool MultiRelay::handleButton(uint8_t b) {
|
||||
unsigned long now = millis();
|
||||
|
||||
//button is not momentary, but switch. This is only suitable on pins whose on-boot state does not matter (NOT gpio0)
|
||||
if (buttons[b].type == BTN_TYPE_SWITCH) {
|
||||
if (buttonType[b] == BTN_TYPE_SWITCH) {
|
||||
//handleSwitch(b);
|
||||
if (buttons[b].pressedBefore != isButtonPressed(b)) {
|
||||
buttons[b].pressedTime = now;
|
||||
buttons[b].pressedBefore = !buttons[b].pressedBefore;
|
||||
if (buttonPressedBefore[b] != isButtonPressed(b)) {
|
||||
buttonPressedTime[b] = now;
|
||||
buttonPressedBefore[b] = !buttonPressedBefore[b];
|
||||
}
|
||||
|
||||
if (buttons[b].longPressed == buttons[b].pressedBefore) return handled;
|
||||
if (buttonLongPressed[b] == buttonPressedBefore[b]) return handled;
|
||||
|
||||
if (now - buttons[b].pressedTime > WLED_DEBOUNCE_THRESHOLD) { //fire edge event only after 50ms without change (debounce)
|
||||
if (now - buttonPressedTime[b] > WLED_DEBOUNCE_THRESHOLD) { //fire edge event only after 50ms without change (debounce)
|
||||
for (int i=0; i<MULTI_RELAY_MAX_RELAYS; i++) {
|
||||
if (_relay[i].button == b) {
|
||||
switchRelay(i, buttons[b].pressedBefore);
|
||||
buttons[b].longPressed = buttons[b].pressedBefore; //save the last "long term" switch state
|
||||
switchRelay(i, buttonPressedBefore[b]);
|
||||
buttonLongPressed[b] = buttonPressedBefore[b]; //save the last "long term" switch state
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -604,40 +604,40 @@ bool MultiRelay::handleButton(uint8_t b) {
|
||||
//momentary button logic
|
||||
if (isButtonPressed(b)) { //pressed
|
||||
|
||||
if (!buttons[b].pressedBefore) buttons[b].pressedTime = now;
|
||||
buttons[b].pressedBefore = true;
|
||||
if (!buttonPressedBefore[b]) buttonPressedTime[b] = now;
|
||||
buttonPressedBefore[b] = true;
|
||||
|
||||
if (now - buttons[b].pressedTime > 600) { //long press
|
||||
if (now - buttonPressedTime[b] > 600) { //long press
|
||||
//longPressAction(b); //not exposed
|
||||
//handled = false; //use if you want to pass to default behaviour
|
||||
buttons[b].longPressed = true;
|
||||
buttonLongPressed[b] = true;
|
||||
}
|
||||
|
||||
} else if (!isButtonPressed(b) && buttons[b].pressedBefore) { //released
|
||||
} else if (!isButtonPressed(b) && buttonPressedBefore[b]) { //released
|
||||
|
||||
long dur = now - buttons[b].pressedTime;
|
||||
long dur = now - buttonPressedTime[b];
|
||||
if (dur < WLED_DEBOUNCE_THRESHOLD) {
|
||||
buttons[b].pressedBefore = false;
|
||||
buttonPressedBefore[b] = false;
|
||||
return handled;
|
||||
} //too short "press", debounce
|
||||
bool doublePress = buttons[b].waitTime; //did we have short press before?
|
||||
buttons[b].waitTime = 0;
|
||||
bool doublePress = buttonWaitTime[b]; //did we have short press before?
|
||||
buttonWaitTime[b] = 0;
|
||||
|
||||
if (!buttons[b].longPressed) { //short press
|
||||
if (!buttonLongPressed[b]) { //short press
|
||||
// if this is second release within 350ms it is a double press (buttonWaitTime!=0)
|
||||
if (doublePress) {
|
||||
//doublePressAction(b); //not exposed
|
||||
//handled = false; //use if you want to pass to default behaviour
|
||||
} else {
|
||||
buttons[b].waitTime = now;
|
||||
buttonWaitTime[b] = now;
|
||||
}
|
||||
}
|
||||
buttons[b].pressedBefore = false;
|
||||
buttons[b].longPressed = false;
|
||||
buttonPressedBefore[b] = false;
|
||||
buttonLongPressed[b] = false;
|
||||
}
|
||||
// if 350ms elapsed since last press/release it is a short press
|
||||
if (buttons[b].waitTime && now - buttons[b].waitTime > 350 && !buttons[b].pressedBefore) {
|
||||
buttons[b].waitTime = 0;
|
||||
if (buttonWaitTime[b] && now - buttonWaitTime[b] > 350 && !buttonPressedBefore[b]) {
|
||||
buttonWaitTime[b] = 0;
|
||||
//shortPressAction(b); //not exposed
|
||||
for (int i=0; i<MULTI_RELAY_MAX_RELAYS; i++) {
|
||||
if (_relay[i].button == b) {
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
# pixels_dice_tray Usermod - BLE Requirement Notice
|
||||
|
||||
## Important: This Usermod Requires Special Configuration
|
||||
|
||||
The `pixels_dice_tray` usermod requires **ESP32 BLE (Bluetooth Low Energy)** support, which is not available in all WLED build configurations.
|
||||
|
||||
### Why is library.json disabled?
|
||||
|
||||
The `library.json` file has been renamed to `library.json.disabled` to prevent this usermod from being automatically included in builds that use `custom_usermods = *` (like the `usermods` environment in platformio.ini).
|
||||
|
||||
The Tasmota Arduino ESP32 platform used by WLED does not include Arduino BLE library by default, which causes compilation failures when this usermod is auto-included.
|
||||
|
||||
### How to Use This Usermod
|
||||
|
||||
This usermod **requires a custom build configuration**. You cannot simply enable it with `custom_usermods = *`.
|
||||
|
||||
1. **Copy the sample configuration:**
|
||||
```bash
|
||||
cp platformio_override.ini.sample ../../../platformio_override.ini
|
||||
```
|
||||
|
||||
2. **Edit `platformio_override.ini`** to match your ESP32 board configuration
|
||||
|
||||
3. **Build with the custom environment:**
|
||||
```bash
|
||||
pio run -e t_qt_pro_8MB_dice
|
||||
# or
|
||||
pio run -e esp32s3dev_8MB_qspi_dice
|
||||
```
|
||||
|
||||
### Platform Requirements
|
||||
|
||||
- ESP32-S3 or compatible ESP32 board with BLE support
|
||||
- Custom platformio environment (see `platformio_override.ini.sample`)
|
||||
- Cannot be used with ESP8266 or ESP32-S2
|
||||
|
||||
### Re-enabling for Custom Builds
|
||||
|
||||
If you want to use this usermod in a custom build:
|
||||
|
||||
1. Rename `library.json.disabled` back to `library.json`
|
||||
2. Manually add it to your custom environment's `custom_usermods` list
|
||||
3. Ensure your platform includes BLE support
|
||||
|
||||
### References
|
||||
|
||||
- See `README.md` for full usermod documentation
|
||||
- See `platformio_override.ini.sample` for build configuration examples
|
||||
@@ -8,10 +8,10 @@
|
||||
#include "dice_state.h"
|
||||
|
||||
// Reuse FX display functions.
|
||||
extern void mode_breath();
|
||||
extern void mode_blends();
|
||||
extern void mode_glitter();
|
||||
extern void mode_gravcenter();
|
||||
extern uint16_t mode_breath();
|
||||
extern uint16_t mode_blends();
|
||||
extern uint16_t mode_glitter();
|
||||
extern uint16_t mode_gravcenter();
|
||||
|
||||
static constexpr uint8_t USER_ANY_DIE = 0xFF;
|
||||
/**
|
||||
@@ -40,8 +40,8 @@ static pixels::RollEvent GetLastRollForSegment() {
|
||||
* Alternating pixels running function (copied static function).
|
||||
*/
|
||||
// paletteBlend: 0 - wrap when moving, 1 - always wrap, 2 - never wrap, 3 - none (undefined)
|
||||
#define PALETTE_SOLID_WRAP (paletteBlend == 1 || paletteBlend == 3)
|
||||
static void running_copy(uint32_t color1, uint32_t color2, bool theatre = false) {
|
||||
#define PALETTE_SOLID_WRAP (strip.paletteBlend == 1 || strip.paletteBlend == 3)
|
||||
static uint16_t running_copy(uint32_t color1, uint32_t color2, bool theatre = false) {
|
||||
int width = (theatre ? 3 : 1) + (SEGMENT.intensity >> 4); // window
|
||||
uint32_t cycleTime = 50 + (255 - SEGMENT.speed);
|
||||
uint32_t it = strip.now / cycleTime;
|
||||
@@ -63,9 +63,10 @@ static void running_copy(uint32_t color1, uint32_t color2, bool theatre = false)
|
||||
SEGENV.aux0 = (SEGENV.aux0 +1) % (theatre ? width : (width<<1));
|
||||
SEGENV.step = it;
|
||||
}
|
||||
return FRAMETIME;
|
||||
}
|
||||
|
||||
static void simple_roll() {
|
||||
static uint16_t simple_roll() {
|
||||
auto roll = GetLastRollForSegment();
|
||||
if (roll.state != pixels::RollState::ON_FACE) {
|
||||
SEGMENT.fill(0);
|
||||
@@ -78,6 +79,7 @@ static void simple_roll() {
|
||||
SEGMENT.setPixelColor(i, SEGCOLOR(1));
|
||||
}
|
||||
}
|
||||
return FRAMETIME;
|
||||
}
|
||||
// See https://kno.wled.ge/interfaces/json-api/#effect-metadata
|
||||
// Name - DieSimple
|
||||
@@ -90,34 +92,31 @@ static void simple_roll() {
|
||||
static const char _data_FX_MODE_SIMPLE_DIE[] PROGMEM =
|
||||
"DieSimple@,,Selected Die;!,!;;1;c1=255";
|
||||
|
||||
static void pulse_roll() {
|
||||
static uint16_t pulse_roll() {
|
||||
auto roll = GetLastRollForSegment();
|
||||
if (roll.state != pixels::RollState::ON_FACE) {
|
||||
mode_breath();
|
||||
return;
|
||||
return mode_breath();
|
||||
} else {
|
||||
mode_blends();
|
||||
uint16_t ret = mode_blends();
|
||||
uint16_t num_segments = float(roll.current_face + 1) / 20.0 * SEGLEN;
|
||||
for (int i = num_segments; i < SEGLEN; i++) {
|
||||
SEGMENT.setPixelColor(i, SEGCOLOR(1));
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
static const char _data_FX_MODE_PULSE_DIE[] PROGMEM =
|
||||
"DiePulse@!,!,Selected Die;!,!;!;1;sx=24,pal=50,c1=255";
|
||||
|
||||
static void check_roll() {
|
||||
static uint16_t check_roll() {
|
||||
auto roll = GetLastRollForSegment();
|
||||
if (roll.state != pixels::RollState::ON_FACE) {
|
||||
running_copy(SEGCOLOR(0), SEGCOLOR(2));
|
||||
return;
|
||||
return running_copy(SEGCOLOR(0), SEGCOLOR(2));
|
||||
} else {
|
||||
if (roll.current_face + 1 >= SEGMENT.custom2) {
|
||||
mode_glitter();
|
||||
return;
|
||||
return mode_glitter();
|
||||
} else {
|
||||
mode_gravcenter();
|
||||
return;
|
||||
return mode_gravcenter();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "pixels_dice_tray",
|
||||
"build": { "libArchive": false},
|
||||
"dependencies": {
|
||||
"arduino-pixels-dice":"https://github.com/axlan/arduino-pixels-dice.git",
|
||||
"BLE":"*"
|
||||
}
|
||||
}
|
||||
@@ -461,11 +461,11 @@ class PixelsDiceTrayUsermod : public Usermod {
|
||||
#if USING_TFT_DISPLAY
|
||||
bool handleButton(uint8_t b) override {
|
||||
if (!enabled || b > 1 // buttons 0,1 only
|
||||
|| buttons[b].type == BTN_TYPE_SWITCH || buttons[b].type == BTN_TYPE_NONE ||
|
||||
buttons[b].type == BTN_TYPE_RESERVED ||
|
||||
buttons[b].type == BTN_TYPE_PIR_SENSOR ||
|
||||
buttons[b].type == BTN_TYPE_ANALOG ||
|
||||
buttons[b].type == BTN_TYPE_ANALOG_INVERTED) {
|
||||
|| buttonType[b] == BTN_TYPE_SWITCH || buttonType[b] == BTN_TYPE_NONE ||
|
||||
buttonType[b] == BTN_TYPE_RESERVED ||
|
||||
buttonType[b] == BTN_TYPE_PIR_SENSOR ||
|
||||
buttonType[b] == BTN_TYPE_ANALOG ||
|
||||
buttonType[b] == BTN_TYPE_ANALOG_INVERTED) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -476,43 +476,43 @@ class PixelsDiceTrayUsermod : public Usermod {
|
||||
static unsigned long buttonWaitTime[2] = {0};
|
||||
|
||||
//momentary button logic
|
||||
if (!buttons[b].longPressed && isButtonPressed(b)) { //pressed
|
||||
if (!buttons[b].pressedBefore) {
|
||||
buttons[b].pressedTime = now;
|
||||
if (!buttonLongPressed[b] && isButtonPressed(b)) { //pressed
|
||||
if (!buttonPressedBefore[b]) {
|
||||
buttonPressedTime[b] = now;
|
||||
}
|
||||
buttons[b].pressedBefore = true;
|
||||
buttonPressedBefore[b] = true;
|
||||
|
||||
if (now - buttons[b].pressedTime > WLED_LONG_PRESS) { //long press
|
||||
if (now - buttonPressedTime[b] > WLED_LONG_PRESS) { //long press
|
||||
menu_ctrl.HandleButton(ButtonType::LONG, b);
|
||||
buttons[b].longPressed = true;
|
||||
buttonLongPressed[b] = true;
|
||||
return true;
|
||||
}
|
||||
} else if (!isButtonPressed(b) && buttons[b].pressedBefore) { //released
|
||||
} else if (!isButtonPressed(b) && buttonPressedBefore[b]) { //released
|
||||
|
||||
long dur = now - buttons[b].pressedTime;
|
||||
long dur = now - buttonPressedTime[b];
|
||||
if (dur < WLED_DEBOUNCE_THRESHOLD) {
|
||||
buttons[b].pressedBefore = false;
|
||||
buttonPressedBefore[b] = false;
|
||||
return true;
|
||||
} //too short "press", debounce
|
||||
|
||||
bool doublePress = buttons[b].waitTime; //did we have short press before?
|
||||
buttons[b].waitTime = 0;
|
||||
bool doublePress = buttonWaitTime[b]; //did we have short press before?
|
||||
buttonWaitTime[b] = 0;
|
||||
|
||||
if (!buttons[b].longPressed) { //short press
|
||||
if (!buttonLongPressed[b]) { //short press
|
||||
// if this is second release within 350ms it is a double press (buttonWaitTime!=0)
|
||||
if (doublePress) {
|
||||
menu_ctrl.HandleButton(ButtonType::DOUBLE, b);
|
||||
} else {
|
||||
buttons[b].waitTime = now;
|
||||
buttonWaitTime[b] = now;
|
||||
}
|
||||
}
|
||||
buttons[b].pressedBefore = false;
|
||||
buttons[b].longPressed = false;
|
||||
buttonPressedBefore[b] = false;
|
||||
buttonLongPressed[b] = false;
|
||||
}
|
||||
// if 350ms elapsed since last press/release it is a short press
|
||||
if (buttons[b].waitTime && now - buttons[b].waitTime > WLED_DOUBLE_PRESS &&
|
||||
!buttons[b].pressedBefore) {
|
||||
buttons[b].waitTime = 0;
|
||||
if (buttonWaitTime[b] && now - buttonWaitTime[b] > WLED_DOUBLE_PRESS &&
|
||||
!buttonPressedBefore[b]) {
|
||||
buttonWaitTime[b] = 0;
|
||||
menu_ctrl.HandleButton(ButtonType::SINGLE, b);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ board_build.partitions = ${esp32.large_partitions}
|
||||
board_build.f_flash = 80000000L
|
||||
board_build.flash_mode = qio
|
||||
monitor_filters = esp32_exception_decoder
|
||||
build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"T-QT-PRO-8MB_dice\"
|
||||
build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=T-QT-PRO-8MB
|
||||
-D CONFIG_LITTLEFS_FOR_IDF_3_2 -D WLED_WATCHDOG_TIMEOUT=0
|
||||
-D ARDUINO_USB_CDC_ON_BOOT=1 -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB")
|
||||
|
||||
@@ -75,7 +75,7 @@ board_build.partitions = ${esp32.large_partitions}
|
||||
board_build.f_flash = 80000000L
|
||||
board_build.flash_mode = qio
|
||||
monitor_filters = esp32_exception_decoder
|
||||
build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_8MB_qspi_dice\"
|
||||
build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=ESP32-S3_8MB_qspi
|
||||
-D CONFIG_LITTLEFS_FOR_IDF_3_2 -D WLED_WATCHDOG_TIMEOUT=0
|
||||
-D ARDUINO_USB_CDC_ON_BOOT=1 -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB")
|
||||
|
||||
@@ -105,7 +105,7 @@ lib_deps = ${esp32s3.lib_deps}
|
||||
# https://github.com/wled-dev/WLED/issues/1382
|
||||
; [env:esp32dev_dice]
|
||||
; extends = env:esp32dev
|
||||
; build_flags = ${common.build_flags} ${esp32.build_flags} -D WLED_RELEASE_NAME=\"ESP32_dice\"
|
||||
; build_flags = ${common.build_flags} ${esp32.build_flags} -D WLED_RELEASE_NAME=ESP32
|
||||
; ; Enable Pixels dice mod
|
||||
; -D USERMOD_PIXELS_DICE_TRAY
|
||||
; lib_deps = ${esp32.lib_deps}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "pov_display",
|
||||
"name:": "pov_display",
|
||||
"build": { "libArchive": false},
|
||||
"platforms": ["espressif32"]
|
||||
}
|
||||
|
||||
@@ -5,37 +5,37 @@ static const char _data_FX_MODE_POV_IMAGE[] PROGMEM = "POV Image@!;;;;";
|
||||
|
||||
static POV s_pov;
|
||||
|
||||
void mode_pov_image(void) {
|
||||
uint16_t mode_pov_image(void) {
|
||||
Segment& mainseg = strip.getMainSegment();
|
||||
const char* segName = mainseg.name;
|
||||
if (!segName) {
|
||||
return;
|
||||
return FRAMETIME;
|
||||
}
|
||||
// Only proceed for files ending with .bmp (case-insensitive)
|
||||
size_t segLen = strlen(segName);
|
||||
if (segLen < 4) return;
|
||||
if (segLen < 4) return FRAMETIME;
|
||||
const char* ext = segName + (segLen - 4);
|
||||
// compare case-insensitive to ".bmp"
|
||||
if (!((ext[0]=='.') &&
|
||||
(ext[1]=='b' || ext[1]=='B') &&
|
||||
(ext[2]=='m' || ext[2]=='M') &&
|
||||
(ext[3]=='p' || ext[3]=='P'))) {
|
||||
return;
|
||||
return FRAMETIME;
|
||||
}
|
||||
|
||||
const char* current = s_pov.getFilename();
|
||||
if (current && strcmp(segName, current) == 0) {
|
||||
s_pov.showNextLine();
|
||||
return;
|
||||
return FRAMETIME;
|
||||
}
|
||||
|
||||
static unsigned long s_lastLoadAttemptMs = 0;
|
||||
unsigned long nowMs = millis();
|
||||
// Retry at most twice per second if the image is not yet loaded.
|
||||
if (nowMs - s_lastLoadAttemptMs < 500) return;
|
||||
if (nowMs - s_lastLoadAttemptMs < 500) return FRAMETIME;
|
||||
s_lastLoadAttemptMs = nowMs;
|
||||
s_pov.loadImage(segName);
|
||||
return;
|
||||
return FRAMETIME;
|
||||
}
|
||||
|
||||
class PovDisplayUsermod : public Usermod {
|
||||
|
||||
@@ -234,11 +234,11 @@ class QuinLEDAnPentaUsermod : public Usermod
|
||||
|
||||
bool oledCheckForNetworkChanges()
|
||||
{
|
||||
if (lastKnownNetworkConnected != WLEDNetwork.isConnected() || lastKnownIp != WLEDNetwork.localIP()
|
||||
if (lastKnownNetworkConnected != Network.isConnected() || lastKnownIp != Network.localIP()
|
||||
|| lastKnownWiFiConnected != WiFi.isConnected() || lastKnownSsid != WiFi.SSID()
|
||||
|| lastKnownApActive != apActive || lastKnownApSsid != apSSID || lastKnownApPass != apPass || lastKnownApChannel != apChannel) {
|
||||
lastKnownNetworkConnected = WLEDNetwork.isConnected();
|
||||
lastKnownIp = WLEDNetwork.localIP();
|
||||
lastKnownNetworkConnected = Network.isConnected();
|
||||
lastKnownIp = Network.localIP();
|
||||
lastKnownWiFiConnected = WiFi.isConnected();
|
||||
lastKnownSsid = WiFi.SSID();
|
||||
lastKnownApActive = apActive;
|
||||
|
||||
@@ -34,7 +34,6 @@ class RgbRotaryEncoderUsermod : public Usermod
|
||||
byte currentColors[3];
|
||||
byte lastKnownBri = 0;
|
||||
|
||||
inline uint32_t colorFromRgb(byte* rgb) { return uint32_t((byte(rgb[0]) << 16) | (byte(rgb[1]) << 8) | (byte(rgb[2]))); }
|
||||
|
||||
void initRotaryEncoder()
|
||||
{
|
||||
@@ -55,14 +54,10 @@ class RgbRotaryEncoderUsermod : public Usermod
|
||||
|
||||
void initLedBus()
|
||||
{
|
||||
// Initialize all pins to the sentinel value first…
|
||||
byte _pins[OUTPUT_MAX_PINS];
|
||||
std::fill(std::begin(_pins), std::end(_pins), 255);
|
||||
// …then set only the LED pin
|
||||
_pins[0] = static_cast<byte>(ledIo);
|
||||
byte _pins[5] = {(byte)ledIo, 255, 255, 255, 255};
|
||||
BusConfig busCfg = BusConfig(TYPE_WS2812_RGB, _pins, 0, numLeds, COL_ORDER_GRB, false, 0);
|
||||
busCfg.iType = BusManager::getI(busCfg.type, busCfg.pins, busCfg.driverType); // assign internal bus type and output driver
|
||||
ledBus = new BusDigital(busCfg);
|
||||
|
||||
ledBus = new BusDigital(busCfg, WLED_MAX_BUSSES - 1);
|
||||
if (!ledBus->isOk()) {
|
||||
cleanup();
|
||||
return;
|
||||
@@ -80,7 +75,7 @@ class RgbRotaryEncoderUsermod : public Usermod
|
||||
for (int i = 0; i < currentPos / incrementPerClick - 1; i++) {
|
||||
ledBus->setPixelColor(i, 0);
|
||||
}
|
||||
ledBus->setPixelColor(currentPos / incrementPerClick - 1, colorFromRgb(currentColors));
|
||||
ledBus->setPixelColor(currentPos / incrementPerClick - 1, colorFromRgbw(currentColors));
|
||||
for (int i = currentPos / incrementPerClick; i < numLeds; i++) {
|
||||
ledBus->setPixelColor(i, 0);
|
||||
}
|
||||
@@ -96,7 +91,7 @@ class RgbRotaryEncoderUsermod : public Usermod
|
||||
if (ledMode == 3) {
|
||||
hsv2rgb((i) / float(numLeds), 1, .25);
|
||||
}
|
||||
ledBus->setPixelColor(i, colorFromRgb(currentColors));
|
||||
ledBus->setPixelColor(i, colorFromRgbw(currentColors));
|
||||
}
|
||||
for (int i = currentPos / incrementPerClick; i < numLeds; i++) {
|
||||
ledBus->setPixelColor(i, 0);
|
||||
|
||||
@@ -143,13 +143,7 @@ void ShtUsermod::appendDeviceToMqttDiscoveryMessage(JsonDocument& root) {
|
||||
device[F("ids")] = escapedMac.c_str();
|
||||
device[F("name")] = serverDescription;
|
||||
device[F("sw")] = versionString;
|
||||
// AI: below section was generated by an AI
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
device[F("mdl")] = ESP.getChipModel();
|
||||
#else
|
||||
device[F("mdl")] = F("ESP8266");
|
||||
#endif
|
||||
// AI: end
|
||||
device[F("mf")] = F("espressif");
|
||||
}
|
||||
|
||||
|
||||
+1
-502
@@ -1,505 +1,4 @@
|
||||
# Usermod user FX
|
||||
|
||||
This usermod is a common place to put various users’ WLED effects. It lets you load your own custom effects or bring back deprecated ones—without touching core WLED source code.
|
||||
This Usermod is a common place to put various user's LED effects.
|
||||
|
||||
Multiple Effects can be specified inside this single usermod, as we will illustrate below. You will be able to define them with custom names, sliders, etc. as with any other Effect.
|
||||
|
||||
* [Installation](./README.md#installation)
|
||||
* [How The Usermod Works](./README.md#how-the-usermod-works)
|
||||
* [Basic Syntax for WLED Effect Creation](./README.md#basic-syntax-for-wled-effect-creation)
|
||||
* [Understanding 2D WLED Effects](./README.md#understanding-2d-wled-effects)
|
||||
* [The Metadata String](./README.md#the-metadata-string)
|
||||
* [Understanding 1D WLED Effects](./README.md#understanding-1d-wled-effects)
|
||||
* [Combining Multiple Effects in this Usermod](./README.md#combining-multiple-effects-in-this-usermod)
|
||||
* [Compiling](./README.md#compiling)
|
||||
* [Change Log](./README.md#change-log)
|
||||
* [Contact Us](./README.md#contact-us)
|
||||
|
||||
## Installation
|
||||
|
||||
To activate the usermod, add the following line to your platformio_override.ini
|
||||
```ini
|
||||
custom_usermods = user_fx
|
||||
```
|
||||
Or if you are already using a usermod, append user_fx to the list
|
||||
```ini
|
||||
custom_usermods = audioreactive user_fx
|
||||
```
|
||||
|
||||
## How The Usermod Works
|
||||
|
||||
The `user_fx.cpp` file can be broken down into four main parts:
|
||||
* **static effect definition** - This is a static LED setting that is displayed if an effect fails to initialize.
|
||||
* **User FX function definition(s)** - This area is where you place the FX code for all of the custom effects you want to use. This mainly includes the FX code and the static variable containing the [metadata string](https://kno.wled.ge/interfaces/json-api/#effect-metadata).
|
||||
* **Usermod Class definition(s)** - The class definition defines the blueprint from which all your custom Effects (or any usermod, for that matter) are created.
|
||||
* **Usermod registration** - All usermods have to be registered so that they are able to be compiled into your binary.
|
||||
|
||||
We will go into greater detail on how custom effects work in the usermod and how to go about creating your own in the section below.
|
||||
|
||||
|
||||
## Basic Syntax for WLED Effect Creation
|
||||
|
||||
WLED effects generally follow a certain procedure for their operation:
|
||||
1. Determine dimension of segment
|
||||
2. Calculate new state if needed
|
||||
3. Implement a loop that calculates color for each pixel and sets it using `SEGMENT.setPixelColor()`
|
||||
4. The function is called at current frame rate.
|
||||
|
||||
Below are some helpful variables and functions to know as you start your journey towards WLED effect creation:
|
||||
|
||||
| Syntax Element | Size | Description |
|
||||
| :---------------------------------------------- | :----- | :---------- |
|
||||
| [`SEGMENT.speed / intensity / custom1 / custom2`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX.h#L450) | 8-bit | These read-only variables help you control aspects of your custom effect using the UI sliders. You can edit these variables through the UI sliders when WLED is running your effect. (These variables can be controlled by the API as well.) Note that while `SEGMENT.intensity` through `SEGMENT.custom2` are 8-bit variables, `SEGMENT.custom3` is actually 5-bit. The other three bits are used by the boolean parameters `SEGMENT.check1` through `SEGMENT.check3` and are bit-packed to conserve data size and memory. |
|
||||
| [`SEGMENT.custom3`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX.h#L454) | 5-bit | Another optional UI slider for custom effect control. While `SEGMENT.speed` through `SEGMENT.custom2` are 8-bit variables, `SEGMENT.custom3` is actually 5-bit. |
|
||||
| [`SEGMENT.check1 / check2 / check3`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX.h#L455) | 1-bit | These variables are boolean parameters which show up as checkbox options in the User Interface. They are bit-packed along with `SEGMENT.custom3` to conserve data size and memory. |
|
||||
| [`SEGENV.aux0 / aux1`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX.h#L467) | 16-bit | These are state variables that persists between function calls, and they are free to be overwritten by the user for any use case. |
|
||||
| [`SEGENV.step`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX.h#L465) | 32-bit | This is a timestamp variable that contains the last update time. It is initially set during effect initialization to 0, and then it updates with the elapsed time after each frame runs. |
|
||||
| [`SEGENV.call`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX.h#L466) | 32-bit | A counter for how many times this effect function has been invoked since it started. |
|
||||
| [`strip.now`](https://github.com/wled/WLED/blob/main/wled00/FX.h) | 32-bit | Current timestamp in milliseconds. (Equivalent to `millis()`, but use `strip.now()` instead.) `strip.now` respects the timebase, which can be used to advance or reset effects in a preset. This can be useful to sync multiple segments. |
|
||||
| [`SEGLEN / SEG_W / SEG_H`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX.h#L116) | 16-bit | These variables are macros that help define the length and width of your LED strip/matrix segment. |
|
||||
| [`SEGPALETTE`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX.h#L115) | --- | Macro that gets the currently selected palette for the currently processing segment. |
|
||||
| [`hw_random8()`](https://github.com/wled/WLED/blob/7b0075d3754fa883fc1bbc9fbbe82aa23a9b97b8/wled00/fcn_declare.h#L548) | 8-bit | One of several functions that generates a random integer. (All of the "hw_" functions are similar to the FastLED library's random functions, but in WLED they use true hardware-based randomness instead of a pseudo random number. In short, they are better and faster.) |
|
||||
| [`SEGCOLOR(x)`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX.h#L114) | 32-bit | Macro that gets user-selected colors from UI, where x is an integer 1, 2, or 3 for primary, secondary, and tertiary colors, respectively. |
|
||||
| [`SEGMENT.setPixelColor`](https://github.com/WLED/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX_fcn.cpp) / [`setPixelColorXY`](https://github.com/WLED/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX_2Dfcn.cpp) | 32-bit | Function that paints one pixel. `setPixelColor` is 1‑D; `setPixelColorXY` expects `(x, y)` and an RGBW color value. |
|
||||
| [`SEGMENT.color_wheel()`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX_fcn.cpp#L1092) | 32-bit | Input 0–255 to get a color. Transitions r→g→b→r. In HSV terms, `pos` is H. Note: only returns palette color unless the Default palette is selected. |
|
||||
| [`SEGMENT.color_from_palette()`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX_fcn.cpp#L1093) | 32-bit | Gets a single color from the currently selected palette for a segment. (This function which should be favoured over `ColorFromPalette()` because this function returns an RGBW color with white from the `SEGCOLOR` passed, while also respecting the setting for palette wrapping. On the other hand, `ColorFromPalette()` simply gets the RGB palette color.) |
|
||||
| [`fade_out()`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX_fcn.cpp#L1012) | --- | fade out function, higher rate = quicker fade. fading is highly dependent on frame rate (higher frame rates, faster fading). each frame will fade at max 9% or as little as 0.8%. |
|
||||
| [`fadeToBlackBy()`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX_fcn.cpp#L1043) | --- | can be used to fade all pixels to black. |
|
||||
| [`fadeToSecondaryBy()`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX_fcn.cpp#L1043) | --- | fades all pixels to secondary color. |
|
||||
| [`move()`](https://github.com/WLED/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX_fcn.cpp) | --- | Moves/shifts pixels in the desired direction. |
|
||||
| [`blur / blur2d`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX_fcn.cpp#L1053) | --- | Blurs all pixels for the desired segment. Blur also has the boolean option `smear`, which, when activated, does not fade the blurred pixel(s). |
|
||||
|
||||
You will see how these syntax elements work in the examples below.
|
||||
|
||||
|
||||
|
||||
## Understanding 2D WLED Effects
|
||||
|
||||
In this section we give some advice to those who are new to WLED Effect creation. We will illustrate how to load in multiple Effects using this single usermod, and we will do a deep dive into the anatomy of a 1D Effect as well as a 2D Effect.
|
||||
(Special thanks to @mryndzionek for offering this "Diffusion Fire" 2D Effect for this tutorial.)
|
||||
|
||||
### Imports
|
||||
The first line of the code imports the [wled.h](https://github.com/wled/WLED/blob/main/wled00/wled.h) file into this module. Importing `wled.h` brings all of the variables, files, and functions listed in the table above (and more) into your custom effect for you to use.
|
||||
|
||||
```cpp
|
||||
#include "wled.h"
|
||||
```
|
||||
|
||||
### Static Effect Definition
|
||||
The next code block is the `mode_static` definition. This is usually left as `SEGMENT.fill(SEGCOLOR(0));` to leave all pixels off if the effect fails to load, but in theory one could use this as a 'fallback effect' to take on a different behavior, such as displaying some other color instead of leaving the pixels off.
|
||||
|
||||
`FX_FALLBACK_STATIC` is a macro that calls `mode_static()` and then returns.
|
||||
|
||||
### User Effect Definitions
|
||||
Pre-loaded in this template is an example 2D Effect called "Diffusion Fire". (This is the name that would be shown in the UI once the binary is compiled and run on your device, as defined in the metadata string.)
|
||||
The effect starts off by checking to see if the segment that the effect is being applied to is a 2D Matrix, and if it is not, then it runs the static effect which displays no pattern:
|
||||
```cpp
|
||||
if (!strip.isMatrix || !SEGMENT.is2D())
|
||||
FX_FALLBACK_STATIC; // not a 2D set-up
|
||||
```
|
||||
The next code block contains several constant variable definitions which essentially serve to extract the dimensions of the user's 2D matrix and allow WLED to interpret the matrix as a 1D coordinate system (WLED must do this for all 2D animations):
|
||||
```cpp
|
||||
const int cols = SEG_W;
|
||||
const int rows = SEG_H;
|
||||
const auto XY = [&](int x, int y) { return x + y * cols; };
|
||||
```
|
||||
* The first line assigns the number of columns (width) in the active segment to cols.
|
||||
* SEG_W is a macro defined in WLED that expands to SEGMENT.width(). This value is the width of your 2D matrix segment, used to traverse the matrix correctly.
|
||||
* Next, we assign the number of rows (height) in the segment to rows.
|
||||
* SEG_H is a macro for SEGMENT.height(). Combined with cols, this allows pixel addressing in 2D (x, y) space.
|
||||
* The third line declares a lambda function named `XY` to map (x, y) matrix coordinates into a 1D index in the LED array. This assumes row-major order (left to right, top to bottom).
|
||||
* This lambda helps with mapping a local 1D array to a 2D one.
|
||||
|
||||
The next lines of code further the setup process by defining variables that allow the effect's settings to be configurable using the UI sliders (or alternatively, through API calls):
|
||||
```cpp
|
||||
const uint8_t refresh_hz = map(SEGMENT.speed, 0, 255, 20, 80);
|
||||
const unsigned refresh_ms = 1000 / refresh_hz;
|
||||
const int16_t diffusion = map(SEGMENT.custom1, 0, 255, 0, 100);
|
||||
const uint8_t spark_rate = SEGMENT.intensity;
|
||||
const uint8_t turbulence = SEGMENT.custom2;
|
||||
```
|
||||
* The first line maps the SEGMENT.speed (user-controllable parameter from 0–255) to a value between 20 and 80 Hz.
|
||||
* This determines how often the effect should refresh per second (Higher speed = more frames per second).
|
||||
* Next we convert refresh rate from Hz to milliseconds. (It’s easier to schedule animation updates in WLED using elapsed time in milliseconds.)
|
||||
* This value is used to time when to update the effect.
|
||||
* The third line utilizes the `custom1` control (0–255 range, usually exposed via sliders) to define the diffusion rate, mapped to 0–100.
|
||||
* This controls how much "heat" spreads to neighboring pixels — more diffusion = smoother flame spread.
|
||||
* Next we assign `SEGMENT.intensity` (user input 0–255) to a variable named `spark_rate`.
|
||||
* This controls how frequently new "spark" pixels appear at the bottom of the matrix.
|
||||
* A higher value means more frequent ignition of flame points.
|
||||
* The final line stores the user-defined `custom2` value to a variable called `turbulence`.
|
||||
* This is used to introduce randomness in spark generation or flow — more turbulence means more chaotic behavior.
|
||||
|
||||
Next we will look at some lines of code that handle memory allocation and effect initialization:
|
||||
|
||||
```cpp
|
||||
unsigned dataSize = cols * rows; // SEGLEN (virtual length) is equivalent to vWidth()*vHeight() for 2D
|
||||
```
|
||||
* This part calculates how much memory we need to represent per-pixel state.
|
||||
* `cols * rows` or `(or SEGLEN)` returns the total number of pixels in the current segment.
|
||||
* This fire effect models heat values per pixel (not just colors), so we need persistent storage — one uint8_t per pixel — for the entire effect.
|
||||
> **_NOTE:_** Virtual lengths `vWidth()` and `vHeight()` will be evaluated differently based on your own custom effect, and based on what other settings are active. For example: If you have an LED strip of length = 60 and you enable grouping = 2, then the virtual length will be 30, so the FX will render 30 pixels instead of 60. This is also true for mirroring or adding gaps--it halves the size. For a 1D strip mapped to 2D, the virtual length depends on selected mode. Keep these things in mind during your custom effect's creation.
|
||||
|
||||
```cpp
|
||||
if (!SEGENV.allocateData(dataSize))
|
||||
FX_FALLBACK_STATIC; // allocation failed
|
||||
```
|
||||
* Upon the first call, this section allocates a persistent data buffer tied to the segment environment (`SEGENV.data`). All subsequent calls simply ensure that the data is still valid.
|
||||
* The syntax `SEGENV.allocateData(n)` requests a buffer of size n bytes (1 byte per pixel here).
|
||||
* If allocation fails (e.g., out of memory), it returns false, and the effect can’t proceed.
|
||||
* It calls previously defined `mode_static()` fallback effect, which just fills the segment with a static color. We need to do this because WLED needs a fail-safe behavior if a custom effect can't run properly due to memory constraints.
|
||||
|
||||
|
||||
The next lines of code clear the LEDs and initialize timing:
|
||||
```cpp
|
||||
if (SEGENV.call == 0) {
|
||||
SEGMENT.fill(BLACK);
|
||||
SEGENV.step = 0;
|
||||
}
|
||||
```
|
||||
* The first line checks whether this is the first time the effect is being run; `SEGENV.call` is a counter for how many times this effect function has been invoked since it started.
|
||||
* If `SEGENV.call` equals 0 (which it does on the very first call, making it useful for initialization), then it clears the LED segment by filling it with black (turns off all LEDs).
|
||||
* This gives a clean starting point for the fire animation.
|
||||
* It also initializes `SEGENV.step`, a timing marker, to 0. This value is later used as a timestamp to control when the next animation frame should occur (based on elapsed time).
|
||||
|
||||
|
||||
The next block of code is where the animation update logic starts to kick in:
|
||||
```cpp
|
||||
if ((strip.now - SEGENV.step) >= refresh_ms) {
|
||||
uint8_t tmp_row[cols]; // Keep for ≤~1 KiB; otherwise consider heap or reuse SEGENV.data as scratch.
|
||||
SEGENV.step = strip.now;
|
||||
// scroll up
|
||||
for (unsigned y = 1; y < rows; y++)
|
||||
for (unsigned x = 0; x < cols; x++) {
|
||||
unsigned src = XY(x, y);
|
||||
unsigned dst = XY(x, y - 1);
|
||||
SEGENV.data[dst] = SEGENV.data[src];
|
||||
}
|
||||
```
|
||||
* The first line checks if it's time to update the effect frame. `strip.now` is the current timestamp in milliseconds; `SEGENV.step` is the last update time (set during initialization or previous frame). `refresh_ms` is how long to wait between frames, computed earlier based on SEGMENT.speed.
|
||||
* The conditional statement in the first line of code ensures the effect updates on a fixed interval — e.g., every 20 ms for 50 Hz.
|
||||
* The second line of code declares a temporary row buffer for intermediate diffusion results that is one byte per column (horizontal position), so this buffer holds one row's worth of heat values.
|
||||
* You'll see later that it writes results here before updating `SEGENV.data`.
|
||||
* Note: this is allocated on the stack each frame. Keep such VLAs ≤ ~1 KiB; for larger sizes, prefer a buffer in `SEGENV.data`.
|
||||
|
||||
> **_IMPORTANT NOTE:_** Creating variable‑length arrays (VLAs) is non‑standard C++, but this practice is used throughout WLED and works in practice. But be aware that VLAs live on the stack, which is limited. If the array scales with segment length (1D), it can overflow the stack and crash. Keep VLAs ≲ ~1 KiB; an array with 4000 LEDs is ~4 KiB and will likely crash. It’s worse with `uint16_t`. Anything larger than ~1 KiB should go into `SEGENV.data`, which has a higher limit.
|
||||
|
||||
|
||||
Now we get to the spark generation portion, where new bursts of heat appear at the bottom of the matrix:
|
||||
```cpp
|
||||
if (hw_random8() > turbulence) {
|
||||
// create new sparks at bottom row
|
||||
for (unsigned x = 0; x < cols; x++) {
|
||||
uint8_t p = hw_random8();
|
||||
if (p < spark_rate) {
|
||||
unsigned dst = XY(x, rows - 1);
|
||||
SEGENV.data[dst] = 255;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
* The first line randomizes whether we even attempt to spawn sparks this frame.
|
||||
* `hw_random8()` gives a random number between 0–255 using a fast hardware RNG.
|
||||
* `turbulence` is a user-controlled parameter (SEGMENT.custom2, set earlier).
|
||||
* Higher turbulence means this block is less likely to run (because `hw_random8()` is less likely to exceed a high threshold).
|
||||
* This adds randomness to when sparks appear — simulating natural flicker and chaotic fire.
|
||||
* The next line loops over all columns in the bottom row (row `rows - 1`).
|
||||
* Another random number, `p`, is used to probabilistically decide whether a spark appears at this (x, `rows-1`) position.
|
||||
* Next is a conditional statement. The lower spark_rate is, the fewer sparks will appear.
|
||||
* `spark_rate` comes from `SEGMENT.intensity` (0–255).
|
||||
* High intensity means more frequent ignition.
|
||||
* `dst` calculates the destination index in the bottom row at column x.
|
||||
* The final line here sets the heat at this pixel to maximum (255).
|
||||
* This simulates a fresh burst of flame, which will diffuse and move upward over time in subsequent frames.
|
||||
|
||||
Next we reach the first part of the core of the fire simulation, which is diffusion (how heat spreads to neighboring pixels):
|
||||
```cpp
|
||||
// diffuse
|
||||
for (unsigned y = 0; y < rows; y++) {
|
||||
for (unsigned x = 0; x < cols; x++) {
|
||||
unsigned v = SEGENV.data[XY(x, y)];
|
||||
if (x > 0) {
|
||||
v += SEGENV.data[XY(x - 1, y)];
|
||||
}
|
||||
if (x < (cols - 1)) {
|
||||
v += SEGENV.data[XY(x + 1, y)];
|
||||
}
|
||||
tmp_row[x] = min(255, (int)(v * 100 / (300 + diffusion)));
|
||||
}
|
||||
```
|
||||
* This block of code starts by looping over each row from top to bottom. (We will do diffusion for each pixel row.)
|
||||
* Next we start an inner loop which iterates across each column in the current row.
|
||||
* Starting with the current heat value of pixel (x, y) assigned `v`:
|
||||
* if there’s a pixel to the left, add its heat to the total.
|
||||
* If there’s a pixel to the right, add its heat as well.
|
||||
* So essentially, what the two `if` statements accomplish is: `v = center + left + right`.
|
||||
* The final line of code applies diffusion smoothing:
|
||||
* The denominator controls how much the neighboring heat contributes. `300 + diffusion` means that with higher diffusion, you get more smoothing (since the sum is divided more).
|
||||
* The `v * 100` scales things before dividing (preserving some dynamic range).
|
||||
* `min(255, ...)` clamps the result to 8-bit range.
|
||||
* This entire line of code stores the smoothed heat into the temporary row buffer.
|
||||
|
||||
After calculating tmp_row, we now handle rendering the pixels by updating the actual segment data and turning 'heat' into visible colors:
|
||||
```cpp
|
||||
for (unsigned x = 0; x < cols; x++) {
|
||||
SEGENV.data[XY(x, y)] = tmp_row[x];
|
||||
if (SEGMENT.check1) {
|
||||
uint32_t color = SEGMENT.color_from_palette(tmp_row[x], true, false, 0);
|
||||
SEGMENT.setPixelColorXY(x, y, color);
|
||||
} else {
|
||||
uint32_t base = SEGCOLOR(0);
|
||||
SEGMENT.setPixelColorXY(x, y, color_fade(base, tmp_row[x]));
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
* This next loop starts iterating over each row from top to bottom. (We're now doing this for color-rendering for each pixel row.)
|
||||
* Next we update the main segment data with the smoothed value for this pixel.
|
||||
* The if statement creates a conditional rendering path — the user can toggle this. If `check1` is enabled in the effect metadata, we use a color palette to display the flame.
|
||||
* The next line converts the heat value (`tmp_row[x]`) into a `color` from the current palette with 255 brightness, and no wrapping in palette lookup.
|
||||
* This creates rich gradient flames (e.g., yellow → red → black).
|
||||
* Finally we set the rendered color for the pixel (x, y).
|
||||
* This repeats for each pixel in each row.
|
||||
* If palette use is disabled, we fallback to fading a base color.
|
||||
* `SEGCOLOR(0)` gets the first user-selected color for the segment.
|
||||
* The final line of code fades that base color according to the heat value (acts as brightness multiplier).
|
||||
|
||||
* Even though the effect logic itself controls when to update based on refresh_ms, WLED will still call this function at roughly FRAMETIME intervals (the FPS limit set in config) to check whether an update is needed. If nothing needs to change, the frame still needs to be re-rendered so color or brightness transitions will be smooth.
|
||||
|
||||
If you want to run your effect at a fixed frame rate you can use the following code to not update your effect state, be aware however that transitions for your effect will also run at this frame rate - for example if you limit your effect to say 5 FPS, brightness changes and color changes may not look smooth. Also `SEGMENT.call` is still incremented on each function call.
|
||||
```cpp
|
||||
//limit update rate
|
||||
if (strip.now - SEGENV.step < FRAMETIME_FIXED) return;
|
||||
SEGENV.step = strip.now;
|
||||
```
|
||||
|
||||
### The Metadata String
|
||||
At the end of every effect is an important line of code called the **metadata string**.
|
||||
It defines how the effect is to be interacted with in the UI:
|
||||
```cpp
|
||||
static const char _data_FX_MODE_DIFFUSIONFIRE[] PROGMEM = "Diffusion Fire@!,Spark rate,Diffusion Speed,Turbulence,,Use palette;;Color;;2;pal=35";
|
||||
```
|
||||
This metadata string is passed into `strip.addEffect()` and parsed by WLED to determine how your effect appears and behaves in the UI.
|
||||
The string follows the syntax of `<Effect Parameters>;<Colors>;<Palette>;<Flags>;<Defaults>`, where Effect Parameters are specified by a comma-separated list.
|
||||
The values for Effect Parameters will always follow the convention in the table below:
|
||||
|
||||
| Parameter | Default tooltip label |
|
||||
| :-------- | :-------------------- |
|
||||
| sx | Effect Speed |
|
||||
| ix | Effect Intensity |
|
||||
| c1 | Custom 1 |
|
||||
| c2 | Custom 2 |
|
||||
| c3 | Custom 3 |
|
||||
| o1 | Checkbox 1 |
|
||||
| o2 | Checkbox 2 |
|
||||
| o3 | Checkbox 3 |
|
||||
|
||||
Using this info, let’s split the Metadata string above into logical sections:
|
||||
|
||||
| Syntax Element | Description |
|
||||
| :---------------------------------------------- | :---------- |
|
||||
| "Diffusion Fire@! | Name. (The @ symbol marks the end of the Effect Name, and the beginning of the Parameter String elements.) |
|
||||
| !, | Use default UI entry; for the first space, this will automatically create a slider for Speed |
|
||||
| Spark rate, Diffusion Speed, Turbulence, | UI sliders for Spark Rate, Diffusion Speed, and Turbulence. Defining slider 2 as "Spark Rate" overwrites the default value of Intensity. |
|
||||
| (blank), | unused (empty field with not even a space) |
|
||||
| Use palette; | This occupies the spot for the 6th effect parameter, which automatically makes this a checkbox argument `o1` called Use palette in the UI. When this is enabled, the effect uses `SEGMENT.color_from_palette(...)` (RGBW-aware, respects wrap), otherwise it fades from `SEGCOLOR(0)`. The first semicolon marks the end of the Effect Parameters and the beginning of the `Colors` parameter. |
|
||||
| Color; | Custom color field `(SEGCOLOR(0))` |
|
||||
| (blank); | Empty means the effect does not allow Palettes to be selected by the user. But used in conjunction with the checkbox argument, palette use can be turned on/off by the user. |
|
||||
| 2; | Flag specifying that the effect requires a 2D matrix setup |
|
||||
| pal=35" | Default Palette ID. this is the setting that the effect starts up with. |
|
||||
|
||||
More information on metadata strings can be found [here](https://kno.wled.ge/interfaces/json-api/#effect-metadata).
|
||||
|
||||
|
||||
## Understanding 1D WLED Effects
|
||||
|
||||
Next, we will look at a 1D WLED effect called `Sinelon`. This one is an especially interesting example because it shows how a single effect function can be used to create several different selectable effects in the UI.
|
||||
We will break this effect down step by step.
|
||||
(This effect was originally one of the FastLED example effects; more information on FastLED can be found [here](https://fastled.io/).)
|
||||
|
||||
```cpp
|
||||
static void sinelon_base(bool dual, bool rainbow=false) {
|
||||
```
|
||||
* The first line of code defines `sinelon base` as static helper function. This is how all effects are initially defined.
|
||||
* Notice that it has some optional flags; these parameters will allow us to easily define the effect in different ways in the UI.
|
||||
|
||||
```cpp
|
||||
if (SEGLEN <= 1) FX_FALLBACK_STATIC;
|
||||
```
|
||||
* If segment length ≤ 1, there’s nothing to animate. Just show static mode.
|
||||
|
||||
The line of code helps create the "Fade Out" Trail:
|
||||
```cpp
|
||||
SEGMENT.fade_out(SEGMENT.intensity);
|
||||
```
|
||||
* Gradually dims all LEDs each frame using SEGMENT.intensity as fade amount.
|
||||
* Creates the trailing "comet" effect by leaving a fading path behind the moving dot.
|
||||
|
||||
Next, the effect computes some position information for the actively changing pixel, and the rest of the pixels as well:
|
||||
```cpp
|
||||
unsigned pos = beatsin16_t(SEGMENT.speed/10, 0, SEGLEN-1);
|
||||
if (SEGENV.call == 0) SEGENV.aux0 = pos;
|
||||
```
|
||||
* Calculates a sine-based oscillation to move the dot smoothly back and forth.
|
||||
* `beatsin16_t` is an improved version of FastLED’s beatsin16 function, generating smooth oscillations
|
||||
* SEGMENT.speed / 10: affects oscillation speed. Higher = faster.
|
||||
* 0: minimum position.
|
||||
* SEGLEN-1: maximum position.
|
||||
* On first call `(SEGENV.call == 0)`, stores initial position in `SEGENV.aux0`. (`SEGENV.aux0` is a temporary state variable to keep track of last position.)
|
||||
|
||||
The next lines of code help determine the colors to be used:
|
||||
```cpp
|
||||
uint32_t color1 = SEGMENT.color_from_palette(pos, true, false, 0);
|
||||
uint32_t color2 = SEGCOLOR(2);
|
||||
```
|
||||
* `color1`: main moving dot color, chosen from palette using the current position as index.
|
||||
* `color2`: secondary color from user-configured color slot 2.
|
||||
|
||||
The next part takes into account the optional argument for if a Rainbow colored palette is in use:
|
||||
```cpp
|
||||
if (rainbow) {
|
||||
color1 = SEGMENT.color_wheel((pos & 0x07) * 32);
|
||||
}
|
||||
```
|
||||
* If `rainbow` is true, override color1 using a rainbow wheel, producing rainbow cycling colors.
|
||||
* `(pos & 0x07) * 32` ensures the color changes gradually with position.
|
||||
|
||||
```cpp
|
||||
SEGMENT.setPixelColor(pos, color1);
|
||||
```
|
||||
* Lights up the computed position with the selected color.
|
||||
|
||||
The next line takes into account another one of the optional arguments for the effect to potentially handle dual mirrored dots which create the animation:
|
||||
```cpp
|
||||
if (dual) {
|
||||
if (!color2) color2 = SEGMENT.color_from_palette(pos, true, false, 0);
|
||||
if (rainbow) color2 = color1; // share rainbow color
|
||||
SEGMENT.setPixelColor(SEGLEN-1-pos, color2);
|
||||
}
|
||||
```
|
||||
* If dual is true:
|
||||
* Uses `color2` for mirrored dot on opposite side.
|
||||
* If `color2` is not set (0), fallback to same palette color as `color1`.
|
||||
* In `rainbow` mode, force both dots to share the rainbow color.
|
||||
* Sets pixel at `SEGLEN-1-pos` to `color2`.
|
||||
|
||||
This final part of the effect function will fill in the 'trailing' pixels to complete the animation:
|
||||
```cpp
|
||||
if (SEGENV.aux0 < pos) {
|
||||
for (unsigned i = SEGENV.aux0; i < pos ; i++) {
|
||||
SEGMENT.setPixelColor(i, color1);
|
||||
if (dual) SEGMENT.setPixelColor(SEGLEN-1-i, color2);
|
||||
}
|
||||
} else {
|
||||
for (unsigned i = SEGENV.aux0; i > pos ; i--) {
|
||||
SEGMENT.setPixelColor(i, color1);
|
||||
if (dual) SEGMENT.setPixelColor(SEGLEN-1-i, color2);
|
||||
}
|
||||
}
|
||||
SEGENV.aux0 = pos;
|
||||
}
|
||||
```
|
||||
* The first line checks if current position has changed since last frame. (Prevents holes if the dot moves quickly and "skips" pixels.) If the position has changed, then it will implement the logic to update the rest of the pixels.
|
||||
* Fills in all pixels between previous position (SEGENV.aux0) and new position (pos) to ensure smooth continuous trail.
|
||||
* Works in both directions: Forward (if new pos > old pos), and Backward (if new pos < old pos).
|
||||
* Updates `SEGENV.aux0` to current position at the end.
|
||||
|
||||
The last part of this effect has the Wrapper functions for different Sinelon modes.
|
||||
Notice that there are three different modes that we can define from the single effect definition by leveraging the arguments in the function:
|
||||
```cpp
|
||||
void mode_sinelon(void) {
|
||||
sinelon_base(false);
|
||||
}
|
||||
// Calls sinelon_base with dual = false and rainbow = false
|
||||
|
||||
void mode_sinelon_dual(void) {
|
||||
sinelon_base(true);
|
||||
}
|
||||
// Calls sinelon_base with dual = true and rainbow = false
|
||||
|
||||
void mode_sinelon_rainbow(void) {
|
||||
sinelon_base(false, true);
|
||||
}
|
||||
// Calls sinelon_base with dual = false and rainbow = true
|
||||
```
|
||||
|
||||
And then the last part defines the metadata strings for each effect to specify how it will be portrayed in the UI:
|
||||
```cpp
|
||||
static const char _data_FX_MODE_SINELON[] PROGMEM = "Sinelon@!,Trail;!,!,!;!";
|
||||
static const char _data_FX_MODE_SINELON_DUAL[] PROGMEM = "Sinelon Dual@!,Trail;!,!,!;!";
|
||||
static const char _data_FX_MODE_SINELON_RAINBOW[] PROGMEM = "Sinelon Rainbow@!,Trail;,,!;!";
|
||||
```
|
||||
Refer to the section above for guidance on understanding metadata strings.
|
||||
|
||||
|
||||
### The UserFxUsermod Class
|
||||
|
||||
The `UserFxUsermod` class registers the `mode_diffusionfire` effect with WLED. This section starts right after the effect function and metadata string, and is responsible for making the effect usable in the WLED interface:
|
||||
```cpp
|
||||
class UserFxUsermod : public Usermod {
|
||||
private:
|
||||
public:
|
||||
void setup() override {
|
||||
strip.addEffect(255, &mode_diffusionfire, _data_FX_MODE_DIFFUSIONFIRE);
|
||||
|
||||
////////////////////////////////////////
|
||||
// add your effect function(s) here //
|
||||
////////////////////////////////////////
|
||||
|
||||
// use id=255 for all custom user FX (the final id is assigned when adding the effect)
|
||||
|
||||
// strip.addEffect(255, &mode_your_effect, _data_FX_MODE_YOUR_EFFECT);
|
||||
// strip.addEffect(255, &mode_your_effect2, _data_FX_MODE_YOUR_EFFECT2);
|
||||
// strip.addEffect(255, &mode_your_effect3, _data_FX_MODE_YOUR_EFFECT3);
|
||||
}
|
||||
void loop() override {} // nothing to do in the loop
|
||||
uint16_t getId() override { return USERMOD_ID_USER_FX; }
|
||||
};
|
||||
```
|
||||
* The first line declares a new class called UserFxUsermod. It inherits from `Usermod`, which is the base class WLED uses for any pluggable user-defined modules.
|
||||
* This makes UserFxUsermod a valid WLED extension that can hook into `setup()`, `loop()`, and other lifecycle events.
|
||||
* The `void setup()` function runs once when WLED initializes the usermod.
|
||||
* It's where you should register your effects, initialize hardware, or do any other setup logic.
|
||||
* `override` ensures that this matches the Usermod base class definition.
|
||||
* The `strip.addEffect` line is an important one that registers the custom effect so WLED knows about it.
|
||||
* 255: Temporary ID — WLED will assign a unique ID automatically. (**Create all custom effects with the 255 ID.**)
|
||||
* `&mode_diffusionfire`: Pointer to the effect function.
|
||||
* `_data_FX_MODE_DIFFUSIONFIRE`: Metadata string stored in PROGMEM, describing the effect name and UI fields (like sliders).
|
||||
* After this, your custom effect shows up in the WLED effects list.
|
||||
* The `loop()` function remains empty because this usermod doesn’t need to do anything continuously. WLED still calls this every main loop, but nothing is done here.
|
||||
* If your usermod had to respond to input or update state, you'd do it here.
|
||||
* The last part returns a unique ID constant used to identify this usermod.
|
||||
* USERMOD_ID_USER_FX is defined in [const.h](https://github.com/wled/WLED/blob/main/wled00/const.h). WLED uses this for tracking, debugging, or referencing usermods internally.
|
||||
|
||||
The final part of this file handles instantiation and initialization:
|
||||
```cpp
|
||||
static UserFxUsermod user_fx;
|
||||
REGISTER_USERMOD(user_fx);
|
||||
```
|
||||
* The first line creates a single, global instance of your usermod class.
|
||||
* The last line is a macro that tells WLED: “This is a valid usermod — load it during startup.”
|
||||
* WLED adds it to the list of active usermods, calls `setup()` and `loop()`, and lets it interact with the system.
|
||||
|
||||
|
||||
|
||||
## Combining Multiple Effects in this Usermod
|
||||
|
||||
So now let's say that you wanted add the effects "Diffusion Fire" and "Sinelon" through this same Usermod file:
|
||||
* Navigate to [the code for Sinelon](https://github.com/wled/WLED/blob/7b0075d3754fa883fc1bbc9fbbe82aa23a9b97b8/wled00/FX.cpp#L3110).
|
||||
* Copy this code, and place it below the metadata string for Diffusion Fire. Be sure to get the metadata string as well--and to name it something different than what's already inside the core WLED code. (Refer to the metadata String section above for more information.)
|
||||
* Register the effect using the `addEffect` function in the Usermod class.
|
||||
* Compile the code!
|
||||
|
||||
## Compiling
|
||||
Compiling WLED yourself is beyond the scope of this tutorial, but [the complete guide to compiling WLED can be found here](https://kno.wled.ge/advanced/compiling-wled/), on the official WLED documentation website.
|
||||
|
||||
## Change Log
|
||||
|
||||
### Version 1.0.0
|
||||
|
||||
* First version of the custom effect creation guide
|
||||
|
||||
## Contact Us
|
||||
|
||||
This custom effect tutorial guide is still in development.
|
||||
If you have suggestions on what should be added, or if you've found any parts of this guide which seem incorrect, feel free to reach out [here](mailto:aregis1992@gmail.com) and help us improve this guide for future creators.
|
||||
|
||||
+16
-1210
File diff suppressed because it is too large
Load Diff
@@ -96,14 +96,14 @@ public:
|
||||
fastled_col.red = colPri[0];
|
||||
fastled_col.green = colPri[1];
|
||||
fastled_col.blue = colPri[2];
|
||||
prim_hsv = rgb2hsv(fastled_col);
|
||||
prim_hsv = rgb2hsv_approximate(fastled_col);
|
||||
new_val = (int16_t)prim_hsv.h + fadeAmount;
|
||||
if (new_val > 255)
|
||||
new_val -= 255; // roll-over if bigger than 255
|
||||
if (new_val < 0)
|
||||
new_val += 255; // roll-over if smaller than 0
|
||||
prim_hsv.h = (byte)new_val;
|
||||
fastled_col = prim_hsv ;
|
||||
hsv2rgb_rainbow(prim_hsv, fastled_col);
|
||||
colPri[0] = fastled_col.red;
|
||||
colPri[1] = fastled_col.green;
|
||||
colPri[2] = fastled_col.blue;
|
||||
@@ -121,14 +121,14 @@ public:
|
||||
fastled_col.red = colPri[0];
|
||||
fastled_col.green = colPri[1];
|
||||
fastled_col.blue = colPri[2];
|
||||
prim_hsv = rgb2hsv(fastled_col);
|
||||
prim_hsv = rgb2hsv_approximate(fastled_col);
|
||||
new_val = (int16_t)prim_hsv.h - fadeAmount;
|
||||
if (new_val > 255)
|
||||
new_val -= 255; // roll-over if bigger than 255
|
||||
if (new_val < 0)
|
||||
new_val += 255; // roll-over if smaller than 0
|
||||
prim_hsv.h = (byte)new_val;
|
||||
fastled_col = prim_hsv;
|
||||
hsv2rgb_rainbow(prim_hsv, fastled_col);
|
||||
colPri[0] = fastled_col.red;
|
||||
colPri[1] = fastled_col.green;
|
||||
colPri[2] = fastled_col.blue;
|
||||
|
||||
@@ -71,7 +71,6 @@ String HttpPullLightControl::generateUniqueId() {
|
||||
unsigned char shaResult[20]; // SHA1 produces a hash of 20 bytes (which is 40 HEX characters)
|
||||
mbedtls_sha1_context ctx;
|
||||
mbedtls_sha1_init(&ctx);
|
||||
#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0)
|
||||
status = mbedtls_sha1_starts_ret(&ctx);
|
||||
if (status != 0) {
|
||||
DEBUG_PRINTLN(F("Error starting SHA1 checksum calculation"));
|
||||
@@ -84,21 +83,6 @@ String HttpPullLightControl::generateUniqueId() {
|
||||
if (status != 0) {
|
||||
DEBUG_PRINTLN(F("Error finishing SHA1 checksum calculation"));
|
||||
}
|
||||
#else
|
||||
// function names have changed in esp-idf V5
|
||||
status = mbedtls_sha1_starts(&ctx);
|
||||
if (status != 0) {
|
||||
DEBUG_PRINTLN(F("Error starting SHA1 checksum calculation"));
|
||||
}
|
||||
status = mbedtls_sha1_update(&ctx, reinterpret_cast<const unsigned char*>(input.c_str()), input.length());
|
||||
if (status != 0) {
|
||||
DEBUG_PRINTLN(F("Error feeding update buffer into ongoing SHA1 checksum calculation"));
|
||||
}
|
||||
status = mbedtls_sha1_finish(&ctx, shaResult);
|
||||
if (status != 0) {
|
||||
DEBUG_PRINTLN(F("Error finishing SHA1 checksum calculation"));
|
||||
}
|
||||
#endif
|
||||
mbedtls_sha1_free(&ctx);
|
||||
|
||||
// Convert the Hash to a hexadecimal string
|
||||
@@ -300,6 +284,7 @@ void HttpPullLightControl::handleResponse(String& responseStr) {
|
||||
if (!requestJSONBufferLock(myLockId)) {
|
||||
DEBUG_PRINT(F("ERROR: Can not request JSON Buffer Lock, number: "));
|
||||
DEBUG_PRINTLN(myLockId);
|
||||
releaseJSONBufferLock(); // Just release in any case, maybe there was already a buffer lock
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user