Compare commits

...

449 Commits

Author SHA1 Message Date
Will Tatam d4ce7cd6ad 16.0.1-rc1 2026-06-20 11:18:33 +01:00
Damian Schneider 2f15fdc167 Prevent arduino core from assigning default MISO pin (#5672)
* add invalid dummy pin if miso is undefined to prevent default fallback
2026-06-19 19:25:33 +02:00
Damian Schneider 4d8b73e16e fix color jump if changing mid transition (#5601)
* fix color jump if changing mid transition
2026-06-19 19:22:59 +02:00
Damian Schneider 505292b94e apply gamma during realtime override (#5666) 2026-06-19 19:20:56 +02:00
Damian Schneider fb55a5059e Restore pre 16.0 looks on several FX (#5684)
* restore pre 16.0 looks on several FX
2026-06-19 19:18:17 +02:00
Damian Schneider 15699eba90 Fix LED glitches on long strips for C3 (#5688)
* increase wait time, remove unnecessary FPS check
2026-06-19 17:26:04 +02:00
Damian Schneider 76c966d0ed bugfix in segment deconstructor (#5687)
* bugfix in segment deconstructor
* make stopTransition() nullpointer safe
2026-06-19 17:23:55 +02:00
Damian Schneider 4c250a8700 Rename LED Types: CCT instead of CW (#5612)
* rename CW LED descriptions to more commonly used CCT
* reorder, make naming more consistent
* unify LED type naming
* remove TYPE_WS2812_2CH_X3 as it is not implemented and same as FW1906
2026-06-18 12:44:43 +02:00
Frank c5fade4da2 fix example buildenvs in platformio_override.sample.ini
* all buildenvs need WLED_RELEASE_NAME
* 8266 needs either WLED_DISABLE_PARTICLESYSTEM1D or WLED_DISABLE_PARTICLESYSTEM2D
* removed obsolete esp32 "V4" builds
* removed duplicate buildenvs that are 100% the same as standard buildenvs
2026-06-16 17:20:50 +02:00
Frank 81f660f390 fix parse error in platformio_override.sample.ini
added custom_usermods to [8266], so user env can reference it
2026-06-16 17:19:22 +02:00
Benjam Welker 1103c91c5b Analog button fix (#5659)
* add analog button options

* fix issues with duplicate options for digital vs analog

* dynamic button type options
2026-06-15 21:25:03 +02:00
Damian Schneider 6f849fdc03 Update pixel buffer after changing matrix dimensions (#5675)
* update pixel buffer after changing matrix dimensions
2026-06-15 21:08:57 +02:00
Will Tatam 76ef0d3998 docs: refresh README to reflect current feature set and project state (#5626)
* docs: refresh README to reflect current feature set and project state

* docs: fix HUB75 support note — available on all ESP32 variants, not just S3

* docs: mention network audio sources in AudioReactive feature description

* docs: remove ESP8266 references — EOL platform, not recommended for new installs

* docs: make PayPal donation link clearly attributed to Aircoookie

* docs: add cross-references to kno.wled.ge docs throughout feature list
2026-06-15 12:58:15 +02:00
Damian Schneider 06e6b23314 bugfix in parsePacket(): accept short artnet packets (#5588) for 16_x
* bugfix in parsePacket(): accept short artnet packets
* add min artnet packet size check and fix indentation

---------

Co-authored-by: Frank Möhle <91616163+softhack007@users.noreply.github.com>
2026-06-09 14:42:46 +02:00
Frank Möhle 0828173147 disable fb infer in coderabbit tools list 2026-06-09 14:14:11 +02:00
Frank Möhle 0d4abd2764 Remove outdated Wiki link from CONTRIBUTING.md
Removed link to WIKI for submitting a PR. The information in the wiki is outdated, and it contradicts the "no force-push" rule.
2026-06-09 14:14:10 +02:00
Frank Möhle 1694d383d8 Update links in CONTRIBUTING.md to reference AGENTS.md 2026-06-09 14:14:10 +02:00
Frank Möhle 4d2ad7ace4 Clarify branch maintenance status in instructions
Updated branch descriptions for clarity and maintenance status.
2026-06-09 14:14:10 +02:00
Frank Möhle a8105caa98 Clarify repository language requirement
Emphasize that the repository language is English for all aspects including code, comments, and documentation.
2026-06-05 22:36:09 +02:00
Frank e8820d71e7 Update package-lock.json
fix for outdated version
2026-06-04 23:57:05 +02:00
Frank Möhle cd51a4f157 Update recommendations for synchronization methods
added std::atomic
2026-06-04 23:53:45 +02:00
Frank Möhle 2eb492c3a2 Update verification instructions for AI-generated code
limit "existence" check to preprocessor macros etc.
If a global variable or function does not exist, this would lead to a compilation error.
2026-06-04 16:44:26 +02:00
Frank Möhle 6a44bc01a2 Merge pull request #5665 from wled/hub75_gamma_hotfix
HUB75 hotfix: prevents double-applying color corrections (WLED gamma + HUB75 internal CIE correction), by disabling driver-internal color corrections.
2026-06-04 15:04:08 +02:00
Frank e869a89bad platformio: allow building with espressif framework (instead of tasmota)
* add necessary platform and platform_packages entries (commented out)
* compatibility patch for ADC_ATTEN_DB_11
2026-05-31 22:19:43 +02:00
Copilot 0ca86641ef Pin andelf/nightly-release tospecific commit SHA instead of @main branch (#5386)
The nightly release GitHub Action is pinned from the mutable main branch to a specific immutable commit SHA

we should update the pin when v2 comes out.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
Co-authored-by: Frank Möhle <91616163+softhack007@users.noreply.github.com>
2026-05-30 23:55:58 +02:00
Frank Möhle dafde84338 Update frequency details for usermod loop()
Clarified frequency of calls for usermod loop() based on system load.
2026-05-30 15:15:11 +02:00
Frank Möhle 53e01e8934 Remove duplicate sentence in secure code instructions
Removed redundant text regarding destination capacity and length safety.
2026-05-30 02:38:24 +02:00
Frank Möhle 4c385d4b4b Restructure usermod documentation for AI, and clarify details 2026-05-30 02:38:24 +02:00
Frank 6f1907cbdd more robustness for improv - gracefully handle broken input 2026-05-30 00:19:13 +02:00
Frank Möhle ec8369d557 Update caveats and pitfalls in cpp.instructions.md
WLED-MM backport: Clarify operator precedence in C/C++.
2026-05-29 20:28:44 +02:00
Frank Möhle 2e34fd421a small update 2026-05-29 19:56:36 +02:00
Frank Möhle 47d569f3f2 Modify verification step for numerical accuracy
small clarifications:
* not only applicable to fixed-point
* added sentence to explain why this rule is needed.
2026-05-29 19:56:36 +02:00
Frank Möhle c770e37226 Update auth checks for '/reset' endpoint clarification
Clarified the handling of the '/reset' endpoint in the auth checks section - not security critical because it will not alter any user data.
2026-05-29 18:31:59 +02:00
kaibae19 9af95ecd0b fix(FX): restore palette wrap in color_wheel() (regression since 0.15.x) (#5646)
color_wheel() historically called color_from_palette() with moving=true, so
under the default palette blend mode (paletteBlend=0, "wrap if moving") the
palette's end->start seam was interpolated. Commit ee9ac947 changed that call
to moving=false, dropping the wrap; effects that scroll a palette (e.g. the
Palette effect with Retro Clown) then shows the unblended seam travelling
visibly along the strip light-by-light.

Restore the original moving=true (per maintainer review). This fixes the seam
for the Palette effect and every other color_wheel() caller under the default
blend mode, without adding any new function parameters.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 16:45:41 +02:00
Benjam Welker 1bcf58abcd fix button label regression (#5636)
* fix button label regression, also add switch notation
2026-05-29 16:45:41 +02:00
Frank Möhle 317d0ea87b HUB75: use FM6124 driver for 4-scan panels, remove 64x64 limitation if board has PSRAM (partial solution for #5628) (#5647)
* HUB75: use FM6124 driver for 4-scan panels
* Remove 1-panel limit if the board has PSRAM
2026-05-29 16:36:12 +02:00
Copilot dbe8196137 add secure coding guides for AI reviews (#5572)
Added security review guidelines and a short checklist covering critical security areas including buffer safety, input validation, authentication, secure defaults, and protection against common vulnerabilities. Refined rule wording and priorities to better fit WLED’s technical constraints and realistic deployment model.

The lists are based on the OWASP "top 10" from https://github.com/github/awesome-copilot/blob/main/instructions/security-and-owasp.instructions.md, and on lessons learned from past reviews.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: softhack007 <91616163+softhack007@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-05-29 14:00:40 +02:00
Frank 863213f2e7 human-readable wifi events (WLED_DEBUG)
added some missing event names.
2026-05-28 00:07:10 +02:00
Frank 7f640a9dfd extra debug info for wifi disconnect / teardown / setTxPower
should help with debugging connection problems.
2026-05-27 23:28:44 +02:00
Frank 41056f6a3b small addition to anti-slop rules
AI tools often delete code, remove comments, revert recent changes, or modify files that are out-of-scope w.r.t. the PR objectives.
2026-05-27 20:38:29 +02:00
Frank 4e98f97cda adding coderabbit checks for typical AI slop 2026-05-25 22:17:58 +02:00
Frank a01add2d26 update usermod guidelines, update example with "be nice but not too nice" pattern 2026-05-25 21:40:21 +02:00
Frank 87fd423336 move some generic AI rules from copilot-instructions.md to AGENTS.md 2026-05-25 21:40:21 +02:00
Frank Möhle 9dec64c394 Remove 'V5_C6' from the list of base branches
the V5-C6 branch will be merged back into V5 soon.
2026-05-23 23:42:54 +02:00
Frank Möhle 1e3569f78a coderabbit: Add base branches to auto_review configuration 2026-05-23 23:42:54 +02:00
Will Tatam 493b6b7e0f Fix revert button #5574 2026-05-23 19:39:30 +01:00
Will Tatam 500250c117 fix: esp8266_2m_min 2026-05-23 19:39:30 +01:00
Frank Möhle ac1757badd Document branch and release structure in AGENTS.md
Added branch and release structure information to AGENTS.md.
2026-05-23 15:08:48 +02:00
Frank Möhle 69244a8bcf clarify scope of agents.md
Introduction added
2026-05-23 15:08:48 +02:00
Damian Schneider 2f9af5023c add inverse gamma for original looks 2026-05-22 07:53:52 +02:00
Damian Schneider 16c0a27d3b Make twinkle FX look more like it was in the past 2026-05-22 07:53:52 +02:00
Frank Möhle c7100bdaa5 Update AGENTS.md with ESP32 task synchronization guidelines
* Added guidelines for ESP32 task synchronization
* Clarified that PRs must not create file listed in `.gitignore`
2026-05-21 22:05:48 +02:00
Frank Möhle 77937d3a27 Align AGENTS.md with main
fixes some mis-edits from previous commit.
2026-05-21 22:03:10 +02:00
Frank 6e7607102c coding guides: clatify how to check for PSRAM
psramFound() alone is not definite -> nust also check that PSRAM size > 0 to be sure

https://github.com/wled/WLED/issues/5629#issuecomment-4497230168
2026-05-20 12:40:50 +02:00
Frank Möhle a9cb41d767 Add PSRAM guidelines to AGENTS.md
Added PSRAM guidelines including availability checks, DMA compatibility, fragmentation considerations, and performance recommendations.
2026-05-20 12:40:50 +02:00
Frank Möhle 6685ffb774 Update PSRAM guidelines in esp-idf instructions
Clarify PSRAM availability check instructions and usage.
2026-05-20 12:40:50 +02:00
Damian Schneider 8f53447c73 use DRAM as fallback if PSRAM fails 2026-05-20 08:37:09 +02:00
Frank 75faf5efc0 fix esp32s3_4M_none buildenv
tasmota core does not support "dio" mode on S3
2026-05-19 12:43:11 +02:00
Frank Möhle e05e975ce1 Add esp32s3dev_8MB_none to platformio_release.ini.template 2026-05-19 12:29:55 +02:00
Frank Möhle e9f8897772 Add new environments for ESP32-S3 without PSRAM
esp32s3_8MB_none, esp32s3_4M_none
2026-05-19 12:29:55 +02:00
Logan Davis c75fce13d0 Fix KIT-VE PHY address from 0 to 1 (#5618) 2026-05-16 10:36:35 +02:00
Frank Möhle 8300f4d2b4 Audioreactive bugfix: auto-suspend in all realtime modes, but stay active when "Use main segment only" (#5599)
* fixes a minor problem with newer realtime modes (DDP, TPM2NET and DMX) not causing auto-suspend of sound processing.
* ensure that AR audio stays active when "Use main segment only" (other segments are still controlled locally)
* small update for better compatibility with V5 builds
2026-05-15 21:00:34 +02:00
Frank Möhle 75cd8b9fb0 Merge pull request #5608 from wled/softhack007-patch-1
Add sanity checks for ethernet config table and WLED_ETH_DEFAULT
2026-05-12 13:57:49 +02:00
Joeboyc2 a8788a2c89 fix(Fix_unreachable_netservices_v2): v16 compatibility fixes (#5590)
Two issues prevented this usermod from building under v16:

1. library.json is missing "build": {"libArchive": false}, causing the
   build system to reject it with an error before compilation begins.

2. serializeConfig() no longer has a no-argument overload in v16; the
   working replacement is serializeConfigToFS() which creates the JSON
   document internally and writes to the filesystem.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 11:29:42 +02:00
recliq 95e6efc164 Fixed top-LED update logic in grav* audioreactive effects (#5602)
Fix topLED fallrate not responding to "Rate of fall" slider.
2026-05-11 19:47:56 +02:00
Frank Möhle 5fcabec9be Update accessibility section in web instructions
Clarify target browser/platform combinations for WLED web UI.
2026-05-11 19:47:56 +02:00
Frank Möhle fceca393b7 Add accessibility guidelines for WLED web UI
Added section on accessibility and interaction for the WLED web UI.
2026-05-11 19:47:56 +02:00
Will Tatam bd45e1ca90 docs: clarify when usermod IDs are required
Add explicit guidance explaining that a unique USERMOD_ID_* is only
needed when a usermod uses inter-mod communication, pin ownership via
pinManager, or needs to be identifiable in JSON info output. Updates
both AGENTS.md and the const.h comment block to reflect this.
2026-05-11 19:47:56 +02:00
Will Tatam 3a28eba9f4 no need to build esp8266_2m_min for PRs 2026-05-07 20:33:12 +01:00
copilot-swe-agent[bot] 0ad958ae17 Add esp8266_2m_min minimal build env for ESP02 OTA recovery
Agent-Logs-Url: https://github.com/wled/WLED/sessions/9d593605-e26a-4bea-b5dd-913d174d5942

Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2026-05-07 20:33:12 +01:00
copilot-swe-agent[bot] ecc7f33907 Initial plan 2026-05-07 20:33:12 +01:00
Will Tatam f08b8b648f Change 'Latest release' to 'Latest version' in update.htm 2026-05-06 08:57:43 +01:00
Will Tatam 81b588f1e4 Improve update page UX and rename OTA button for consistency
- Release name on update page links directly to the matching bin on download.wled.me
- Badge links to GitHub releases page as 'View latest'
- Auto-update button renamed to 'Auto update'
- Manual upload section gets its own subheading; submit button renamed to 'Upload'
- 'Manual OTA Update' button in security settings renamed to 'Update WLED' to match info menu wording
2026-05-06 08:57:43 +01:00
Will Tatam 09384e4bdb Automatically download the right bin for the latest release and install 2026-05-06 08:57:43 +01:00
Will Tatam 6922938e15 Remove usermods from release 2026-05-06 08:57:43 +01:00
Frank Möhle 362fc664aa Add guideline for providing verifiable citations
Anti-hallucination rule: Emphasize the importance of providing verifiable citations in analyses or recommendations.
2026-05-05 14:53:16 +02:00
Will Tatam 527edf3283 16.0.1-dev 2026-05-03 21:31:45 +01:00
Will Tatam 4374f013f5 16.0.0 2026-05-03 21:13:08 +01:00
Will Tatam f90990b869 Upload then add notes 2026-05-03 21:12:50 +01:00
Will Tatam 7d999a6d61 16.0.0-rc1 2026-05-03 21:12:50 +01:00
Damian Schneider dd0a83f9d1 align settings overview page width with rest of config pages 2026-05-03 21:45:30 +02:00
BobLoeffler68 dd01940cf5 Fixes issue in Dissolve when a palette has same color pixel as the current background color when Complete is enabled (#5453)
* Fixes issue in Dissolve when a palette has same color pixel as the current background color when Complete mode is enabled

---------

Co-authored-by: Damian Schneider <daedae@gmx.ch>
2026-05-03 21:28:24 +02:00
Will Tatam 0d93ca8bac Merge pull request #5553 from intermittech/add-quinled-v4-ethernet-profiles
Add Ethernet profiles for QuinLED Dig-Uno/Quad v4 and Dig-Octa 32-8L v4
2026-05-03 18:55:02 +01:00
Will Tatam a083217176 move platformio_release.ini.template to .github/ for cleaner project root 2026-05-03 18:42:43 +01:00
Will Tatam 86a218e2b4 Skip GPIO validation for HUB75 types in pin checks and clarify slot usage in getNumberOfPins logic 2026-05-03 18:42:43 +01:00
Will Tatam 28b441917b Update platformio configurations for 16MB ESP32-S3 MoonHub (HUB75) board 2026-05-03 18:42:43 +01:00
Will Tatam 129c68c6c9 Add support for HD-WF2 ESP32-S3 (HUB75 without PSRAM) 2026-05-03 18:42:43 +01:00
Will Tatam 923a1ec729 fix(ui): derive hasPSRAM from psrSz capability, not free bytes
Free PSRAM (info.psram) can be transiently 0 on PSRAM-capable boards,
which would wrongly reject multi-panel HUB75 setups in the LED settings
validation. Prefer info.psrSz (total PSRAM size) and fall back to
info.psram only if psrSz is unavailable.

Suggested in PR #5558 review.
2026-05-03 18:42:43 +01:00
Will Tatam 2ceeb67007 Relax panel limit for devices with PSRAM 2026-05-03 18:42:43 +01:00
Will Tatam 13aa7ed787 ci: build HUB75 envs in release workflow
Move the four HUB75 envs from platformio_override.sample.ini into
platformio.ini (refactored with a shared [hub75] section and extends=
to deduplicate), and gate them behind a release-only config template
so nightly/PR CI keep building only the standard env matrix.

The release workflow now copies platformio_release.ini.template into
place, which extends default_envs with the HUB75 envs.
2026-05-03 18:42:43 +01:00
Will Tatam 68640f5dd8 Fix AudioReactive palette names to avoid duplication with usermod prefix
Since the display format is now "UMName: palName", shorten the palette
names to just "Ratio", "Hue", and "Spectrum" — rendering as
"AudioReactive: Ratio" etc. rather than "AudioReactive: Audio Responsive Ratio".
2026-05-03 18:41:50 +01:00
Will Tatam 8a653ebb29 Use usermod name prefix for usermod-supplied palette names
Palette display names now follow the format "UMName: palName" (e.g.
"AudioReactive: Audio Responsive Hue") when a usermod supplies a
specific palName, keeping the usermod name as a consistent prefix.
Fallback when palName is nullptr remains "UMName index".

Update AudioReactive palette names to the full descriptive names from
the original WLED-MM port: "Audio Responsive Ratio", "Audio Responsive
Hue", and "Audio Responsive Spectrum".
2026-05-03 18:41:50 +01:00
Will Tatam dc5ee3f2fa Add name to usermod palette 2026-05-03 18:41:50 +01:00
Damian Schneider 5dc627ef5a remove unneeded check 2026-05-03 18:41:50 +01:00
Damian Schneider 520d8704c5 fix typo 2026-05-03 18:41:50 +01:00
Damian Schneider 55219980c6 rename removeUsermodPalettes 2026-05-03 18:41:50 +01:00
Damian Schneider 6dcae505a0 apply default pal before bounds check 2026-05-03 18:41:50 +01:00
Damian Schneider 52d897ed7a rename and bugfixes 2026-05-03 18:41:50 +01:00
Damian Schneider ef354d0119 add usermod palettes to fix AR palette indexing 2026-05-03 18:41:50 +01:00
Damian Schneider 89aae707d0 fix refresh of custom palettes 2026-05-03 18:41:50 +01:00
Will Miles 25668bb068 Validate gzipped firmware on ESP8266
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-03 18:41:17 +01:00
copilot-swe-agent[bot] 2c1f3c581d Refactor AR init debug logging for network-only sentinel
Agent-Logs-Url: https://github.com/wled/WLED/sessions/85b2a158-f6ba-45cf-88af-0d51b9202cab

Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2026-05-03 15:01:39 +01:00
copilot-swe-agent[bot] 1c8cd15cd1 Fix network-only dmType handling and debug diagnostics
Agent-Logs-Url: https://github.com/wled/WLED/sessions/85b2a158-f6ba-45cf-88af-0d51b9202cab

Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2026-05-03 15:01:39 +01:00
Will Tatam 32de108f4c Clean up redundant preprocessor directive in audio_reactive.cpp 2026-05-03 15:01:39 +01:00
Will Tatam e551ca833b Add audioreactive custom_usermod to multiple ESP8266 environments in platformio.ini now that we support network only option for 8266 2026-05-03 15:01:39 +01:00
Will Tatam 2fa5266dbf Enhance audio reactive logic with improved input validation and new "network receive only" option. 2026-05-03 15:01:39 +01:00
Will Tatam 46dafa2c6b ci: opt into Node.js 24 for GitHub Actions runners
Set FORCE_JAVASCRIPT_ACTIONS_TO_NODE24=true across all affected workflows
to avoid deprecation warnings ahead of the June 2026 forced migration.
2026-05-03 15:01:39 +01:00
Will Tatam 967ef0720c Relax artifact name regex to support forks with different project names 2026-05-03 15:01:39 +01:00
Will Tatam 82489664a0 Name CI artifacts by WLED_RELEASE_NAME instead of PlatformIO env
PR build artifact zips are now named firmware-<RELEASE_NAME> (e.g.
firmware-ESP32.zip) rather than firmware-<pio_env> (e.g.
firmware-esp32dev.zip), making it easier for users to identify the
correct build when testing PRs.
2026-05-03 15:01:39 +01:00
Damian Schneider 9fdd48776d relax restrictions on DDP protocol, minimal check for artNet (#5554) 2026-05-01 22:55:14 +02:00
Damian Schneider 74e0b166ba auto-migration for sunrise/sunset legacy config (#5555)
* add auto-migration for sunris/sunset legacy config
2026-05-01 14:44:07 +02:00
Damian Schneider 61bd8bf5b4 update VERSION 2026-05-01 13:34:40 +02:00
Damian Schneider f79cf568b2 move pixelforge json list and cpt city palettes to wled repo (#5541)
also removed commented code
2026-04-26 03:58:05 +02:00
Damian Schneider c53511b5aa add placeholder to keep custom palette ID's consistent (#5537) 2026-04-25 10:54:07 +02:00
Damian Schneider 0325e30bef cleanup of PR #5503: remove unnecessary changes (#5536) 2026-04-25 10:54:07 +02:00
Will Tatam 456ddee962 Merge pull request #5503 from RobertoD91/main
fix for serial port on esp32 (#5501)
2026-04-25 17:17:15 +01:00
Will Tatam 41e4bbf2bd Merge pull request #5473 from DedeHai/pin_dropdowns
Use drop-downs for pins
2026-04-25 17:17:15 +01:00
Damian Schneider 7f292b9ecd fix indentation 2026-04-22 09:57:21 +02:00
Brandon502 6afc990397 GoL missing return. (#5523) 2026-04-22 06:45:22 +02:00
Damian Schneider 974c15c1d3 Fix blending style options list filter for iOS (#5513)
* fix blending style options list filter for iOS
* fix default fallback
2026-04-20 07:52:18 +02:00
Damian Schneider 96b0c7b4ae better packet queuing & pacing for custom palette live preview (#5515)
* better packet queuing / pacing for custom palette live preview
* fix leak
* remove window prefix from variables
2026-04-20 07:37:33 +02:00
Frank Möhle 6394bec21b Highlight target branch for pull requests
Emphasize the importance of targeting the main branch for PRs.
2026-04-20 15:25:36 +02:00
Frank 79fde56b0e align AI instructions with `main` branch
some files were a bit behind - make sure the same instructions apply everywhere.
2026-04-20 14:27:32 +02:00
Frank Möhle e6077acb78 Update rules for PR descriptions and change logs
Added a guideline to prevent agents from overwriting PR descriptions (see https://github.com/orgs/community/discussions/187027).
2026-04-20 14:25:23 +02:00
Frank Möhle 83dd1b078d Merge pull request #5516 from kilrah/fix_dmx_ident
Add identifier string for DMX realtime mode
2026-04-19 18:44:34 +02:00
Damian Schneider c1e5ac5bd1 bugfixes in PS effects
- fix PS Sparkler for large setups: need 32bit random position, 16bit is not enough
- fix PS Fireworks 1D: need to `break` if no particles are available or it can lead to stalls on large setups
- do not use collisions by default PS Fuzzy Noise: its very slow on larger setups
2026-04-17 15:29:53 +02:00
Damian Schneider 1e42ebf852 mask out flag bits in DDP type for compatibility 2026-04-12 19:50:07 +02:00
Damian Schneider e4a7effdb0 add search icon back(#5500) 2026-04-12 19:08:05 +02:00
Damian Schneider ae36f7e203 use memory aligned allocations, fix bug in FFT magnitude (integer path) (#5499)
* use memory aligned allocations, fix bug in FFT magnitude (integer path)
* assign pointer to globals not local copy
2026-04-12 19:06:16 +02:00
Will Miles 4cdc23a48b Merge pull request #5497 from willmmiles/16_x-dynarray-fixes
0.16 - Backport dynarray fixes from V5 WIP
2026-04-12 11:34:41 -04:00
Will Miles 48ab88e11e Fix up usermod validation again
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-12 10:03:11 -04:00
Will Miles fd890b3d58 dynarray: Support ESP-IDF 2026-04-12 10:03:05 -04:00
Will Miles f994c5e995 validate_modules: Improve performance
Use readelf instead of nm for great speed.
2026-04-12 10:02:59 -04:00
Will Miles c789e3d187 Fix usermod count for LTO 2026-04-12 10:02:53 -04:00
Will Miles fadc75da55 dynarray: Directly apply linker fix
Rather than append a linker file, we edit the upstream supplied ones to
add our section to the binaries.  Works better on all platforms.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-04-12 10:02:47 -04:00
Will Tatam 945896ddb7 Merge pull request #5462 from willmmiles/16_x-unlimited-fxdata-size
Serialize fxdata without ArduinoJSON
2026-04-11 10:42:22 +01:00
Will Tatam 188beec614 Merge pull request #5465 from netmindz/usage-extended
New extended data for usage report
2026-04-11 08:21:06 +01:00
Will Tatam 19947ec07e Merge pull request #5489 from wled/copilot/fix-wled-discovery-issues
Fix Alexa/Hue discovery by correcting SSDP response to match UPnP spec
2026-04-10 18:30:07 +01:00
Damian Schneider 8228c7eea4 bugfix in pixelforge (no blur by default), fix glitch in animated gifs for C3 2026-04-07 23:52:28 +02:00
Will Miles 1aba7057a5 Merge pull request #5477 from willmmiles/16_x-ipv6-filter-oops
Fix leak in blockRouterAdvertisements
2026-04-04 21:21:22 -04:00
Will Miles 227d535404 Fix leak in blockRouterAdvertisements 2026-04-04 21:12:17 -04:00
Damian Schneider da8bfda2f0 bugfix: do not restrict segment inputs to allow trailing strips 2026-04-03 14:22:08 +02:00
Damian Schneider e4f2964bd6 hotfix: critical bug in candle FX, integer issue in flow FX 2026-04-02 18:44:52 +02:00
Will Miles a3f730275a Factor out streaming JSON primitives
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 23:30:32 -04:00
Frank b5556aa977 fix for HUB75 builds
replaces the last remaining FastLED.h with fstled_slim.h
2026-03-30 11:45:30 +02:00
Will Miles 82ddefff87 Remove declaration of removed function
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-03-29 23:48:43 -04:00
Will Miles 56a97a0caa Fix off-by-one in fxdata serializer
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-03-29 23:15:27 -04:00
Will Miles 679bb39922 Serialize fxdata without ArduinoJSON
Eliminates size limit.  Fixes #5458
2026-03-29 22:49:26 -04:00
Frank Möhle b293db6af5 Clarify instructions to always use the correct source code branch
Updated instructions for providing references in analysis results.
2026-03-30 00:38:34 +02:00
Will Tatam 774fbc5fab 16.0.0-beta 2026-03-29 22:35:23 +01:00
Copilot 472fca7486 DDP input: reject packets with unsupported data type or non-display destination (#5390)
* Fix: reject invalid DDP packets with wrong destination or unsupported data type

Co-authored-by: DedeHai <6280424+DedeHai@users.noreply.github.com>
2026-03-29 18:01:11 +02:00
Damian Schneider 42844e4fa8 Adding AR support for C3: use DSP FFT and integer math (#4750)
*  fixed problem on S3: turns out to be mem alignment
* fixed scaling for C3
* code cleanup, added arduinoFFT back in, added ifdefs & description

also added major peak and frequency bin calculation for DSP FFT

* moved ifdefs to correct place, separated sample filter & FFT filter application

post FFT band pass and IIR applied to samples are now separated: I found in testing that applying the sample filter helps with aliasing into base-bands, there is no need to hard-cut the lowest frequencies after FFT.

* changed sample low pass cutoff from 80Hz to 90Hz

better anti-aliasing at minimal loss of base frequency.

* code cleanup and minor speed improvement
- moved scaling of FFT values into fftAddAvg() to use ferwer operations
- added "using" for math types, removing some ifdefs and duplications
2026-03-29 13:18:26 +02:00
Benjam Welker 4a6ff64519 add more macro/timer slots (#5140)
* convert to vectors

* Update wled00/cfg.cpp

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* UI fixes

converted preset entry to select box for ease of use

* no eeprom compatibility

* tighten up code

* fix sunrise/sunset issue

* fix issue with sunrise/sunset/regular selector

* preset dropdowns

* sort before use

* delete timer when preset = 0

* truncate long names and add sort options

* html updates

* fix save bug

* make boxes a bit darker

* add fallback if localStorage is empty

* remove sort option

* code rabbit suggested fixes

* remove css

* restyling

* common

* dry preset loading

* revert missing const

* update deep sleep usermod with new timer struct

* tabs

* Revert "tabs"

This reverts commit fdc01f2f5d.

* remove unneeded clamp checks

* leave index alone

* remove useless check

* fix up await functions

* only fetch localstorage

* inline single line function

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-03-29 13:01:24 +02:00
Damian Schneider f6a43f4dfa Extend scrolling text FX with custom fonts and international UTF-8 character support (#5372)
* new font format: *.wbf with a 12byte header, bit packed data and support for variable char width
* add support to load custom fonts from file system
* UTF-8 to unicode support functions
* support for any unicode offset in char 128-255 enables many international chars
* update built-in fonts with similar but nicer ones
* update pixelforge scrolling text tool with a preview and support for custom fonts
* accompanied by Font Factory tool (pixelforge) to easily create custom fonts from various formats
2026-03-29 12:10:23 +02:00
Damian Schneider b7d2c3cd85 Full fastled replacement (#4615)
* removed fastled dependencies
- copied relevant functions
- optimized some of them for ESP32
* added perlin functions from PR, code cleanup. work in progress
* added hsv2rgb16rainbow function, some cleanup
- new rainbow function is faster and more accurate than original fastled function
* updated conversion functions (now faster), cleanup, optimizations
* code cleanup, moved (most) fastled functions into fastled_fcn.cpp
- resolves licensing issue
- moved PRNG into its own space
- prettified some code
* fixed prng: it now generates a full sequence of random numbers, thx @TripleWhy for pointing out the flaw
* rename to fastled_slim, add hsv2rgb() convenience aliases, fix FX usage
* improve ease8inOutCubic() accuracy
* fix background in twinklefox, minor optimization in PS (always use gamma LUT, no ifs)
* bugfixes in FX, adding white and cct transition to slow transition FX

Co-authored-by: Frank <91616163+softhack007@users.noreply.github.com>
2026-03-29 11:40:16 +02:00
Frank Möhle 20c021d9b8 Clarify instruction on gathering information when unsure 2026-03-28 10:04:40 +01:00
Frank Möhle 9c93cb8cf0 Update copilot instructions for clarity and guidance
Added instructions for handling uncertainty and providing references.
2026-03-28 10:00:31 +01:00
Will Tatam ad5043f7e0 do not keep prompting if there was an error, e.g no wifi 2026-03-28 00:21:37 +00:00
Damian Schneider 5ac282f8b9 Add slow transition FX (over 4hours) (#5379) 2026-03-27 19:53:08 +01:00
Damian Schneider e4351c8979 Increase maximum playlist duration (#5455)
* Add numerical input clamps
* increase playlist item limit to uint32_t: 4294967s (49.7 days)
2026-03-27 19:22:53 +01:00
Will Miles 6a16593be5 Merge pull request #5438 from willmmiles/ipv6-dns-fix
Fix DNS hangs on ESP32
2026-03-26 18:48:57 -04:00
Frank Möhle 78ecd3805e service cleanup after removing return FRAMETIME (#5443)
Clean-up and simplification of strip.service() after individual effect FRAMETIME was removed.
* Activation "_frametime" is now the same for all effects, so effects scheduling based on individual frametime is not needed any more
* Special handling for mode_static is obsolete, because the solid effect does not have a different timing any more
* Ensures a safe fallback segment after servicing to avoid stale state.
* Brightness changes use a capped refresh threshold to avoid excessive redraws.
2026-03-26 12:31:08 +01:00
Damian Schneider 4c7fa0303b load pixel forge tools from json file, improved tools list, bugfixes (#5404)
* load pixel forge tools from json file, bugfixes

- load available external tools from a json file instead of hard-coding them in
- add buttons to update or delete tools (instead of manually deleting files in /edit)
- fixed bug that file system memory was not updated after uploading or deleting files
- fixed a bug where gifs with a space would not display
- fixed bug of page not loading properly (out of sync access to tabSw() )
- remove pixelforge from ESP8266 1m builds (does not fit)
- add inline html page if pixelforge is not built in to avoid 404
2026-03-26 10:28:23 +01:00
Frank d8cb20a9e7 bugfix: prevent _segment_index being misaligned w.r.t. segments vector
prevent that an inactive segment in the segments list causes misalignment between getSegmentID() and actual segments vector.
2026-03-24 15:21:31 +01:00
Frank 710f897a1d strip.service bugfix: avoid losing trigger events
* avoid losing "trigger" events if suspend requested during effect service
* clarify some misleading comments
2026-03-24 11:54:25 +01:00
Will Miles abb4124ab4 Fix formatting 2026-03-22 08:54:41 -04:00
Will Miles 3c7fbabd23 Use correct fix for SHA256 hash string
@coderabbitai caught the problem, but its fix suggestion was out to
lunch on this one.  Passing an unsigned char to String.concat() appends
it as a number instead of appending the character.

Instead, mask the unneceeded bits.
2026-03-22 08:36:06 -04:00
Will Miles bda74bafb8 Fix DNS hangs on ESP32
Discard IPv6 RA packets, which cause LwIP to overwrite the IPv4 DNS
servers, resulting in DNS queries hanging up because there are no
accessible servers.
2026-03-22 07:38:06 -04:00
Damian Schneider 9cf3a7d184 Add stencil blendingmode and minor speed optimisations (#4889)
* Optimisatins in blendSegment()
* adding new blend mode: "Stencil"
2026-03-22 11:29:16 +01:00
Frank 5cef850c47 prevent _usedSegmentData Counter Underflow During Mode Blending
In startTransition(), a copy of the old effect's data buffer is made using raw malloc() — it deliberately bypasses allocateData(). This buffer is never added to the _usedSegmentData static counter (only allocateData() does that). If the old effect then calls allocateData() it causes _usedSegmentData to underflow.

https://github.com/wled/WLED/issues/5427#issuecomment-4103228702
2026-03-22 02:33:25 +01:00
Frank Möhle 74763040ad Hostname creation cleanup, avoid that default DNS name ESP-xxxxx gets used (#5424)
Bug Fixes
* Improved hostname sanitization and buffer-size safeguards to prevent truncation and ensure DNS-compliant names.
* Hostname is now applied earlier during network setup and on reconnects (ESP32), improving connectivity and ensuring settings take effect.
* Solves the problem that ESP32 boards show as "ESP-xxxxxx" on the network, no matter if DNS name is defined or not
* (breaking fix) prevent hostnames with double ``WLED-`` prefix, like ``wled-WLED-kitchen``

New Features
* Added a length-aware hostname retrieval API with an mDNS-preference option and robust fallback behavior.
* Preserved legacy compatibility by providing a length-aware alias for legacy hostname preparation.

Open Ends
* mDNS name is not sanitized when registering the mDNS endpoint, to avoid breaking previous functionality
2026-03-21 17:31:42 +01:00
Damian Schneider 7a635c88d6 hotfix in color_fade() only round up in video mode, let it go to zero otherwise 2026-03-21 15:19:29 +01:00
Damian Schneider dc5a7350d2 improved hue preservation in color fade, should also be faster (#5434) 2026-03-21 12:48:48 +01:00
Will Miles 81f251c125 Merge pull request #5332 from DedeHai/asyncDNS
implement async DNS lookup - no more stuck NTP requests
2026-03-20 13:20:07 -04:00
Damian Schneider ff9067f10b remove debut print, remove whitespaces 2026-03-20 16:38:10 +01:00
Will Miles 144c0f9dd6 Ensure AsyncDNS safety
Make AsyncDNS queries return a future-like object backed by a smart
pointer.  This ensures that the client cannot release the backing
memory while the dns query callback is still holding a reference to it,
preventing any use-after-free cases.
2026-03-19 21:38:45 -04:00
Will Tatam a721264205 Merge pull request #5407 from wled/copilot/allow-upgrade-to-esp32
Allow OTA upgrade between ESP32_V4 and ESP32 release names
2026-03-18 16:44:41 +00:00
Will Miles f28cbea2c2 Metadata compatibility cleanup
- Remove unnecessary checks
- Remove unnecessary constructions
- Fix unsafe buffer-to-String
2026-03-16 18:20:41 -04:00
Will Miles 3b256eed1e Merge branch 'main' into copilot/allow-upgrade-to-esp32 2026-03-16 18:17:51 -04:00
Damian Schneider 610c80a0ae UI improvement: clearer tool icons and added text description (#5425)
* add two new icons (folder, hammer), add text to tool buttons
2026-03-16 06:56:59 +01:00
Frank Möhle aa6cd883c2 Fix typo 2026-03-16 01:09:06 +01:00
Frank Möhle 7006285856 Update language guidelines in copilot instructions
Clarify language requirement for code comments and documentation.
2026-03-16 01:07:41 +01:00
Frank Möhle 7e490fcd28 ensure that targetFPS is set to default on a fresh install (fixes #5408) 2026-03-13 18:55:08 +01:00
Frank Möhle 6f030e540f OTA update page restyling, automatically set download URL based on info.repo (#5419)
* use same style as other settings pages
* Hide "back" button while update is in progress
* set "download latest binary" URL and badge based on info.repo; directly link to "latest" release
* correct bad name of "Security & Updates" page
* ensure that "update bootloader" section get hidden when not supported
2026-03-13 18:44:35 +01:00
Frank Möhle a63a307c6e Update PR review instructions for generated html files
Clarified instructions for PR authors regarding generated files and updating Web UI files.
2026-03-13 18:40:42 +01:00
Frank Möhle b57d51ef19 correction 2026-03-13 17:42:30 +01:00
Frank Möhle 34722aa371 Update instructions for editing html_*.h files
Clarified instructions regarding editing auto-generated HTML files.
2026-03-13 17:41:26 +01:00
Frank Möhle 19c178d0f9 Update development workflow instructions
Clarify development workflow instructions and emphasize that the changes apply to agent mode only.
2026-03-13 17:36:26 +01:00
Frank Möhle 3244d0f593 Clarify handling of generated html_*.h files
Updated instructions regarding generated html_*.h files to clarify that they should not be committed, and they don't need to be re-generated in any PR.
2026-03-13 17:29:07 +01:00
Frank 64f3aa96dd replace depricated ADC constant
according to espressif docs:
`ADC_ATTEN_DB_12` is deprecated, it behaves the same as `ADC_ATTEN_DB_12`
2026-03-12 16:01:43 +01:00
Frank f816b5fa98 bugfix #5295 - change remaining references to strip.paletteBlend into paletteBlend
these were leftover after refactoring paletteBlend into a global variable.

see #5295 for details
2026-03-12 16:00:20 +01:00
Frank Möhle e4cd730654 Info page: Total LEDs, GitHub repo, minor re-styling (#5418)
* Info page updates and minor re-styling
  * added GitHub repo (link)
  * added Total LEDs
  *  removed lwip major version on esp32
  * two horizontal lines for better readability

* add rel="noopener noreferrer" for improved security
  * When using target="_blank", it's a security best practice to include rel="noopener noreferrer" to prevent the new page from accessing window.opener.
2026-03-11 18:07:19 +01:00
BobLoeffler68 c7fa496fda Spinning Wheel FX in the user_fx usermod (#5293)
* Added the Spinning Wheel effect into the user_fx usermod

* Fixed integer overflow when storing color scale in aux1. And added a comment about the velocity scaling.

* Additions/changes:
* Added Color Per Block checkbox. Enabled will set the spinner LEDs to the same color (instead of changing colors depending on the palette and LED position).
* Added Sync Restart checkbox.  Enabled means that all spinners will restart together (instead of individually) after they have all stopped spinning.
* Added resizing the spinner slider (between 1 and 10 LEDs).
* Changed how we do random speed and slowdown start time (sliders set to 0 = random).
* tweaks here and there

* One minor fix for the spinner colors

* Changed the two analogRead() to hw_random16()

* Changes from SEGLEN to vstripLen suggested by coderabbitai, but it's not working correctly now. Committing and pushing so coderabbitai can check the latest code.

* Rolled back changes from vstripLen to SEGLEN as that is what works correctly.  Also changed to the global paletteBlend.

* Fixed a color issue by using ColorFromPaletteWLED() instead of color_from_palette(). Also removed color_wheel() and the Color Mode option as it's very similar to the new color function. And now using strips variable instead of SEGMENT.nrOfVStrips() after the initial assignment at the top of the code.

* Added the ability to spin the wheel(s)/spinner(s) with a push button or the new Spin Me checkbox.

* Set default of check1 to 1 so it will automatically spin.
2026-03-11 18:01:56 +01:00
Joachim Dick 40442551ef New smooth effect: Color Clouds (#5268)
* New Effect: Color Clouds

ColorClouds: Random start points for clouds and color

ColorClouds: new config option 'More red'

* ColorClouds: Incorporated review comments

- Support for color palettes
- Use perlin16() instead of inoise16()
- Use cos8_t() instead of cos8()

* ColorClouds: incorporated more review comments

* ColorClouds: incorporated final review comment
2026-03-11 08:19:42 +01:00
Will Miles cfa93c9778 Merge pull request #5416 from willmmiles/usermod-validation-path-matching
Fix usermod validation portability
2026-03-10 16:01:51 -04:00
Will Tatam c35358c005 Display relase on info page 2026-03-10 19:07:19 +00:00
Will Miles 2ab9659332 Speed up usermod validation
Avoid checking usermods we've already matched, and exit early if we've
got everything.

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-10 10:29:56 -04:00
Will Miles 3bfdb736e3 Fix usermod validation portability
Use proper path analysis instead of bare text matching.

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-10 10:11:44 -04:00
Will Tatam 53d6c4b059 Merge pull request #5172 from wled/copilot/update-install-prompt-messaging
Simplify upgrade reporting prompt with checkbox-based preference
2026-03-08 20:06:05 +00:00
Will Tatam 5beafe0a7d Merge pull request #5403 from willmmiles/custom-usermods-better
custom_usermod improvements
2026-03-08 15:39:15 +00:00
Will Miles 7f44396f7f validate_modules: Support LTO
When LTO is enabled, the map file no longer provides a positive
indication of whether a given object file has contributed to the final
binary.  Instead use nm to parse the debug data in the .elf file.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-03-08 10:04:38 -04:00
copilot-swe-agent[bot] 5e1ae99767 Refactor normalizeReleaseName to use Arduino String for cleaner code
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2026-03-08 09:58:05 +00:00
Will Miles 1da5dc7d18 Merge pull request #5213 from willmmiles/update-source-version-check
Fix upward compatibility of OTA structure version check
2026-03-07 09:35:05 -05:00
Will Miles 05498f2ae4 Apply fixes from code review
h/t @coderabbitai
2026-03-06 23:21:04 -05:00
copilot-swe-agent[bot] 9ae1ef506c Allow OTA upgrade between ESP32_V4 and ESP32 release names
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2026-03-06 07:01:10 +00:00
copilot-swe-agent[bot] cd1c5ca6bd Initial plan 2026-03-06 06:50:44 +00:00
Damian Schneider b07cd45642 Bugfix in PS particle size for advanced particles
perParticleSize was disabled by default which is wrong. Fixed some bugs regarding to this new parameter: it is now set by default in PS, adusted FX accordingly
2026-03-04 20:38:17 +01:00
BobLoeffler68 278a1fb6c1 Lava Lamp FX in the user_fx usermod (#5253)
* Added Lava Lamp effect to user_fx usermod
2026-03-04 10:05:52 +01:00
BobLoeffler68 0e1da4f004 Magma FX in the user_fx usermod (#5360)
* Added Magma effect to user_fx usermod

* Get color from current palette for lava bombs
2026-03-04 06:21:49 +01:00
BobLoeffler68 5f5f468983 Ants FX in the user_fx usermod (#5251)
* Added the Ants effect to the user_fx usermod
2026-03-03 17:13:55 +01:00
GLEDOPTO c604b99a4c add 3 more standard buttons to ESP-NOW Remote (#5400)
maps codes 20, 21 and 22 to presets 5, 6 and 7
2026-03-03 13:18:39 +01:00
BobLoeffler68 5f28406f42 Morse Code FX in the user_fx usermod (#5252)
* Added Morse Code effect to the user_fx usermod

* Added a few comments

* Added punctuation and end-of-message codes, and changing them will force a re-draw.

* Fixed mode name in addEffect

* cosmetic changes

* * removed PROGMEM from letters and numbers arrays.
* changed 1024 to a constexpr named MORSECODE_MAX_PATTERN_SIZE.
* added MORSECODE_MAX_PATTERN_SIZE to build_morsecode_pattern().
* added boundary checked when adding patterns to the array.
*  changed code to allocate per-segment storage for pattern.

* More bounds checking added.

* Added a lookup table for punctuation morse codes.

* Removed PALETTE_MOVING_WRAP macro as it's not used in this effect.

* Moved all static variables to SEGENV.data

* Now using a bit array instead of a bool array

* added a check to see if the pattern is empty

* Added "color by word" option, moved Color modes to a slider and added a couple comments to top comment block.

* Removed return FRAMETIME

* A few changes suggested by coderabbit.

* A few changes suggested by coderabbit
2026-03-02 19:16:43 +01:00
gustebeast bc229b8cb6 Add flashing effect on line clear in TetrisAI_v2. (#5320)
* Add flashing effect on line clear in TetrisAI_v2.
See https://imgur.com/1dsNCVd for a demo.

* Address feedback for tetris flashing effect.

* Address width == 32 case for isLineFull
2026-03-02 19:11:26 +01:00
gustebeast 5790309371 Introduce comet effect usermod with fire particle system (#5347)
* Introduce comet effect usermod with fire particle system

* Introduce comet effect usermod with fire particle system
2026-03-02 19:10:31 +01:00
Will Miles 210b4d8f54 Add external usermod example 2026-03-01 21:35:16 -05:00
Will Miles 19292675d8 Support lib_deps syntax in custom_usermods
Expand the parsing of custom_usermods to accept the same syntax as
lib_deps.  This allows external usermods to appear on the
custom_usermods lines.  Also includes comment handling.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-03-01 21:35:16 -05:00
Will Miles ac1a4dfbfd validate_modules: Remove obsolete code 2026-03-01 19:27:14 -05:00
Frank 7fa15761ae bugfix: prevent array bounds violations due to short WS payload data
Guard against zero-length binary payloads before dereferencing data[0] or data[1]
2026-03-01 22:30:04 +01:00
Frank 35d267109f bugfix: right shift on signed char is unsafe
Fix signed-byte handling in SHA256 hex conversion.

When bootloaderSHA256Cache bytes >= 0x80 are assigned to a signed char, they sign-extend to negative values. Right-shifting negative values produces incorrect results (e.g., 0xFF becomes -1, which shifts incorrectly). This causes wrong hex digit output for non-ASCII hash bytes.
2026-03-01 22:08:09 +01:00
Will Miles 3bfbbab807 Formalize dynarray system
Break out the dynamic array macros to a re-usable component and fix
the implementation.
2026-02-28 22:47:48 -05:00
Will Miles b97b46ae12 Use new section for dynamic arrays
Use a custom linker section to hold dynamic arrays such as the
usermod list.  This provides an extensible solution for wider use
in the future (such as FX or Bus drivers), as well as IDF v5
compatibility.
2026-02-28 22:47:48 -05:00
Frank Möhle 26d2cc971a Merge pull request #5396 from wled/bugfix_5391
avoid I2S resource conflict (aPLL) with ethernet - V4 builds only (fixes #5391)
2026-02-27 17:30:23 +01:00
Will Tatam 3972b6e8fb Merge pull request #5401 from wled/dependabot/npm_and_yarn/minimatch-10.2.4
Bump minimatch from 10.2.2 to 10.2.4
2026-02-27 08:13:54 +00:00
dependabot[bot] 3116e88cdc Bump minimatch from 10.2.2 to 10.2.4
Bumps [minimatch](https://github.com/isaacs/minimatch) from 10.2.2 to 10.2.4.
- [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/minimatch/compare/v10.2.2...v10.2.4)

---
updated-dependencies:
- dependency-name: minimatch
  dependency-version: 10.2.4
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-27 07:45:41 +00:00
Will Tatam 1c2f70598a Update version from 16.0-alpha to 16.0.0-alpha 2026-02-27 07:44:38 +00:00
copilot-swe-agent[bot] 6f65b46b14 Save version when skipping reporting to prevent re-prompting
When users click "Skip reporting" without the checkbox, now saves
the current version to version-info.json. This prevents the prompt
from appearing again until the version actually changes.

Behavior:
- Skip without checkbox: Saves version, will prompt on next version
- Skip with checkbox: Sets neverAsk=true, never prompts again

Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2026-02-26 14:24:25 +00:00
Damian Schneider 1c8dd8ecd2 extended CCT blending: exclusive blend, bugfix in CCT calculation (fixes color jump) (#5382)
- negative blend values create "exclusive" zones where only one channel is on, blending happens in the center, total is always 255 (non additive)
- CCT from RGB has to be calculated from original color to be accurate
- Fixed CCT brightness ad calculate colorbalance before white
- allow new exclusive blending for 2-wire CCT
2026-02-26 10:42:19 +01:00
Will Tatam 55c4288cb6 Merge pull request #5128 from willmmiles/cleanup-bootloader-sha
Cleanup bootloader SHA256 calculation from #4984
2026-02-26 08:54:16 +00:00
Will Tatam 231fb5a7ca Remove default checked state from versionSaveChoice 2026-02-26 08:52:34 +00:00
Damian Schneider 536b40c6f1 Fix start & end of segment not "flowing" in Flow FX (#5392) 2026-02-26 09:07:37 +01:00
Frank d3a8da212c UI improvement: PDM is not a standard I2S microphone
to avoid user confusion, UI now shows "PDM" instead of "I2S PDM"
2026-02-24 12:56:52 +01:00
Frank 522927ea94 fix for I2S ressource conflict with ethernet (aPLL) - V4 builds only
Don't use aPLL clock for I2S (and PDM) microphones.

Ethernet with internal RMII clock (typically on GPIO 0/16/17) uses the Audio PLL (APLL) to generate the 50 MHz reference clock for RMII.

see https://github.com/wled/WLED/issues/5391
2026-02-24 12:35:38 +01:00
Will Tatam 4178d05c6a Merge pull request #5393 from wled/dependabot/npm_and_yarn/multi-8e762eef80
Bump minimatch and nodemon
2026-02-23 20:18:41 +00:00
dependabot[bot] 2d1315f9dc Bump minimatch and nodemon
Bumps [minimatch](https://github.com/isaacs/minimatch) to 10.2.2 and updates ancestor dependency [nodemon](https://github.com/remy/nodemon). These dependencies need to be updated together.


Updates `minimatch` from 3.1.2 to 10.2.2
- [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/minimatch/compare/v3.1.2...v10.2.2)

Updates `nodemon` from 3.1.9 to 3.1.14
- [Release notes](https://github.com/remy/nodemon/releases)
- [Commits](https://github.com/remy/nodemon/compare/v3.1.9...v3.1.14)

---
updated-dependencies:
- dependency-name: minimatch
  dependency-version: 10.2.2
  dependency-type: indirect
- dependency-name: nodemon
  dependency-version: 3.1.14
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-23 09:43:29 +00:00
Damian Schneider 822e07d9f5 fix boot-up wifi pause even with extended scanning
forgot to push this update before merging #5337
2026-02-21 10:12:34 +01:00
Will Tatam 439d4a0e98 Merge pull request #5357 from netmindz/idf_4_4_8
Upgrade platform to 4.4.8
2026-02-20 19:04:39 +00:00
Damian Schneider 1f102ca832 Fix LED-animations briefly pausing at bootup (ESP32 only) (#5337)
* fix bootup output glitch by using NVM stored credentials
* add ifdef guards for ESP8266
2026-02-20 19:57:19 +01:00
Damian Schneider 43e86d0f4d Add Pin Info: overview of used and available Pins (#5361)
* Add pin manager feature with serializePins() API and UI
* update comment about ESP8266 A0 deficiency
* moved pin capabilities to pinmanager

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: DedeHai <6280424+DedeHai@users.noreply.github.com>
2026-02-20 19:52:57 +01:00
Damian Schneider 29e9c73274 Make omggif.js available as an independent resource, improved sequential loading in cpal (#5362)
* take omggif out of pixelforge.htm, improved sequential loading in cpal

* add js_omggif.h to build output list
2026-02-20 19:44:51 +01:00
Frank Möhle 6240ee69fc Merge pull request #5381 from wled/ui_lang_en
fix for missing lang="en" in some UI pages
2026-02-19 22:26:30 +01:00
Damian Schneider 14bd5d615b Improvements to UI settings readability (#5328)
* update all config pages with sections
* some renamig
* reordered LED settings with more natural "workflow"
* add titles and changed some wording / outdated comments
2026-02-19 19:50:55 +01:00
Frank e0441c8876 Bugfix: some UI pages were missing lang="en"
stops some _smart_ browsers from suggesting to "translate from Danish".
2026-02-19 14:11:51 +01:00
Frank 1ee42f0206 change version scheme to Major.minor, drop leading "0."
as agreed in #5249
2026-02-14 23:31:28 +01:00
Damian Schneider d1ed547a7c Improved bus handling: free choice of bus driver in any order and improved memory calculations (#5303)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: DedeHai <6280424+DedeHai@users.noreply.github.com>
2026-02-14 19:00:37 +01:00
Frank Möhle f830ea498c Clean up global variables namespace, save a few 100 bytes of flash (#5368)
* reduce scope of some variables to "static"

these are not used anywhere else. Making them static avoid name conflicts, cleans up the global scope and in some cases allows for better  optimization by the compiler.

* remove unused reference ``tz``from analog clock usermod

* side-catch: remove two "local var shadows global var" warnings

* reduce scope of functions declared globally, but not used anywhere else
Safe to make static
* declared in fcn_declare.h, only used locally in one file
* not declared in fcn_declare.h, only used locally

* HUB75 small optimization
make bit array functions "static inline"
-> better for optimization, saves some bytes because the compiler does not need to preserve a non-inline function copy for external references.

* a few more static functions
as suggested by the rabbit.
2026-02-11 22:24:06 +01:00
Frank ce31d802d5 add esp-idf managed folders to .gitignore 2026-02-11 10:52:29 +01:00
Frank f09c449ad5 clarification
according to espressif docs, this is "maximum TX power" not "TX power"
2026-02-11 00:38:40 +01:00
Frank Möhle 6bebb8b4a8 Merge pull request #5338 from wled-install/patch-3
Classic ESP32: GPIO17 is not needed to be reserved for chips with in-package PSRAM (ESP32-D0WDR2-V3 and ESP32-D0WDHR2-V3)
2026-02-10 17:31:40 +01:00
Frank Möhle e3bc32a823 remove dead (and dangerous) mutex macros in bus_manager.cpp (#5364)
these macros are
* not used any more
* dangerous because they don't time out
* dangerous because they use an internal mutex of the ledc driver (not part of the LEDC API)
2026-02-09 11:14:58 +01:00
Damian Schneider 6b70c6ae91 Remove FRAMETIME return value from all FX (#5314)
* remove "return FRAMETIME" from all FX, fix timing for some FX

- all FX now render every frame, no more "speed up" during transitions

* fix missing return by adding FS_FALLBACK_STATIC macro

* add FX_FALLBACK_STATIC also to user_fx

* remove obsolete seg.next_time
2026-02-09 07:57:49 +01:00
Frank b9138b4300 show experimental MCUs in nodes list
add decoding for experimental MCUs to nodes list, instead of just showing "?".
2026-02-09 00:55:30 +01:00
Frank Möhle 7a157a8c91 Add experimental ESP32-C6 and ESP32-C5 options to bugreport template 2026-02-09 00:27:30 +01:00
flo269 7387baace4 Remove gamma value lower limit to enable inverse gamma correction (#5187)
* Enable inverse gamma correction (gamma < 1.0)

Allow gamma values from 0.1 to 3.0 instead of restricting to > 1.0.
This enables inverse gamma curves for use cases requiring brightened
mid-tones and compressed highlights.

Changes:
- Update backend validation in set.cpp and cfg.cpp to accept 0.1-3.0
- Update HTML form min value from 1 to 0.1

Co-authored-by: Flo <flo@Mac.lan>
Co-authored-by: Damian Schneider <daedae@gmx.ch>
2026-02-08 17:53:54 +01:00
Damian Schneider 80e75139c6 Full WiFi scan and apply BSSID if used (#5351)
* use extended wifi scan (fix Arduino bug for ESP32 family), apply BSSID
* if BSSID changed: force reconnect, add comment to BSSID check
2026-02-08 09:26:36 +01:00
Will Tatam e9ced6ddf1 Merge pull request #5356 from netmindz/welcome-update
Tweak the welcome page to be more appropriate
2026-02-07 20:00:07 +00:00
Frank Möhle 6d1d494cf5 Extend I2S swapped channel workaround to all 4.4.x versions
It's very unlikely that this old bug will ever be fixed in any esp-idf 4.4.x release.
2026-02-07 20:54:44 +01:00
Will Tatam 65daf26aee Disable pixels_dice_tray - incompatible due to BLE dependency 2026-02-07 18:59:01 +00:00
Will Tatam 591f65fcee Upgrade platform to 4.4.8 2026-02-07 18:24:26 +00:00
Frank Möhle 8b6d4134bd Improved Contributor Guidelines & Education (#5348, part1) (#5350)
* Update CONTRIBUTING.md

based on AI proposals in https://github.com/wled/WLED/issues/5348

* Update CONTRIBUTING.md

official style is "GitHub"

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* minor style updates

* styling and perfection

addressing minor recommendations by the rabbit

* cherry-picking some improvement proposals from the rabbit

* extended opening section
* section groups: Getting Started, During Review, After Approval, Coding Guidelines
* removed duplications in AI section
* improve friendly style
* minor corrections

* nitpick

* ok, some more nitpick

* Minor consistency tweak with other headings/sentences

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-02-07 16:37:53 +01:00
Will Tatam 2102bb12f0 Tweak the welcome page to be more appropriate 2026-02-07 15:15:05 +00:00
Frank 747bc2bacd filter out "external" in nigtly build changelog 2026-02-07 13:59:30 +01:00
Will Tatam 4d63b3c8a0 Merge pull request #5353 from netmindz/dmx-output-enabled-error
DMX Output support exclude message incorrectly displayed on builds *with* support
2026-02-06 20:00:17 +00:00
Will Tatam 147384074d Fix the hiding of the lack of dmx output support message for builds that do have support 2026-02-06 19:50:45 +00:00
Frank Möhle 129137d96c Add new build for esp32s3dev_8MB_qspi
It seems that several S3 with 8MB flash have slower qspi PSRAM, and they won't boot with the 8MB_opi firmware.
2026-02-06 12:58:59 +01:00
gustebeast 0120b1ab79 Add user_fx installation instructions and a usermod config example (#5327)
* Update user_fx to include installation instructions and a usermod config example
* Explain installation when using multiple usermods
2026-02-06 07:16:25 +01:00
Frank Möhle c64b1e3e5d Fix typo in CONTRIBUTING.md 2026-02-05 19:51:59 +01:00
Frank Möhle 0c08c27df4 CONTRIBUTING.md minor formatting updates for easier reading 2026-02-05 19:51:14 +01:00
Damian Schneider 2676ac771d apply inverse gamma to segment brightness for better color preservation (#5343) 2026-02-04 06:57:14 +01:00
wled-install 680ef26f4d Update pin_manager.cpp 2026-02-02 20:55:38 +01:00
wled-install eb4bd6fbb1 Update pin_manager.cpp 2026-02-02 18:55:29 +01:00
Damian Schneider e78b4a245d revert debug timing 2026-02-01 22:30:38 +01:00
Damian Schneider 73ae3de5b8 fix suggestions, use blocking version in busmanager for simplicity, fix busmanager bug 2026-02-01 14:24:47 +01:00
Will Miles a6b107e8df Accept rabbit's fixes 2026-01-31 20:33:28 -05:00
Will Miles 6b953d96eb Fix 5168 (#5181)
* Fix slop from 5168

Remove the redundant field from the info structure and report what we
actually want to know.

* Fix psram size estimate

Shorten the name, and round up --  getPsramSize() is the usable size,
not the total size (the difference is allocator overhead).

* Let psram builds work without it

* Remove impossible cases in psram reports
2026-01-31 19:54:03 -05:00
Benjamin Kraus 3d33bae2b8 Add support for WPA-Enterprise (#5194)
* Squashed commit of the following:

commit 70fe1fc76d3d88947d4c9f8b43d58ea90f944230
Author: Benjamin Kraus <ben@benkraus.com>
Date:   Fri Oct 31 20:52:13 2025 -0400

    Added support for enterprise WiFi.

* Updated based on feedback from CodeRabbit.

* Fixed issue with strncmp identified by CodeRabbit.

* Replaced split declaration-then-assignment with a single statement.

* Revert whitespace only changes.

* Move WPA enterprise behind a feature flag.
2026-01-31 19:51:59 -05:00
Will Miles f2df029f26 Disable boot loader debug prints 2026-01-31 19:42:03 -05:00
Will Miles 761eb99e53 Fix update UI
Make sure the correct things are shown.
2026-01-31 19:40:54 -05:00
Will Miles 2434a9624e Ensure bootloader hashes match
Ensure that our calculation function returns the same value as the
image internal hash.
2026-01-31 18:59:14 -05:00
Will Miles 03d0522cf1 Fix null test 2026-01-31 17:24:38 -05:00
Will Miles 642c99a617 Stream bootloader size validation
Use a stateful object to allow bootloader size calculation to operate
on a stream of data blocks instead of requiring a single flat read.
This allows it to work when calculating the bootloader hash as well
as during update validation.

Co-authored-by: Codex <codex@openai.com>
2026-01-31 17:18:40 -05:00
Will Miles 76c25da58e Use bootloader size in hash calculation
Co-authored-by: Codex <codex@openai.com>
2026-01-31 14:11:33 -05:00
Will Miles b51e7b65f9 Factor out bootloader size estimate
Co-authored-by: Codex <codex@openai.com>
2026-01-31 14:04:45 -05:00
Will Miles 78166090bc Bootloader upload validation cleanup
Co-authored-by: Codex <codex@openai.com>
2026-01-31 14:03:22 -05:00
Will Miles 6d788a27b6 Fix heap checks in bootloader update 2026-01-31 14:03:11 -05:00
Damian Schneider 32cda7d793 add forgotten guarding ifdef 2026-01-31 19:51:22 +01:00
Damian Schneider c180a3fe6f implement async DNS lookup - no more stuck NTP requests
- adding a wrapper class for DNS lookup (lwIP)
- replaces blockig WiFi.hostByName() in NTP and virtual Bus DNS look-ups
2026-01-31 19:37:10 +01:00
Damian Schneider 1ca55e42af fix relay not turning on at boot (#5315)
These changes eliminate an elaborate race condition
* add dedicated function to handle on/off and relay
* add clarifying comment on output set order
* add define for relay delay, honor forceOff in all cases
2026-01-31 17:40:53 +01:00
Will Miles ba677d1a32 Merge remote-tracking branch 'upstream/main' into cleanup-bootloader-sha 2026-01-31 09:11:50 -05:00
copilot-swe-agent[bot] 354da8fdc0 Fix automatic reporting to preserve alwaysReport preference
When automatic reporting was triggered, the third parameter (alwaysReport)
was missing in the reportUpgradeEvent call. This caused the preference to
be lost after the first automatic report, requiring the user to be prompted
again on the next upgrade.

Fixed by passing 'true' as the third parameter to maintain the user's
preference for all future automatic reports.

Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2026-01-31 11:26:02 +00:00
Damian Schneider 2c4ed4249d New custom palettes editor (#5010)
* full refactoring, added live preview, better minifying in cdata.js
* update main UI buttons, support for gaps in cpal files, cpal UI cleanup
* fixed some layout issues, added un-ordered cpal deletion
* changed to tab indentation, paste button border color now holds stored color
* fix preview to work properly and some other fixes in UI
* always unfreeze
* new approach to loading iro.js, add harmonic random palette, many fixes.
* decoupling iro.j, update UI of cpal.htm
- load iro.js sequentially
- no parallel requests in cpal.htm
- update UI buttons
- fix showing sequential loading of palettes (using opacity)
- better UX for mobile (larger markers, larger editor)
- various fixes
* small change to buttons
* load iro.js dynamically, remove iro.js from index.htm, revert changes to cdata.js
* improved visibility for very dark/black palettes and markers
2026-01-30 20:35:15 +01:00
copilot-swe-agent[bot] 39bf31d3a1 Change button text from "Report this update" to "Report update"
Per feedback, simplified button text since the checkbox already
provides context about saving choices for future updates.

Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2026-01-30 08:57:31 +00:00
copilot-swe-agent[bot] 50ef8db595 Fix double toast messages and improve error handling
- Remove duplicate toast messages when checkbox is checked
- Show appropriate message based on saveChoice state
- Ensure error toasts are visible after overlay is removed

Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2026-01-30 08:53:30 +00:00
copilot-swe-agent[bot] d4f365e7e5 Simplify prompt to 2 buttons + checkbox per feedback
- Replace 4-button design with 2 buttons: "Report this update" and "Skip reporting"
- Add checkbox "Save my choice for future updates" (checked by default)
- When checkbox is checked:
  - "Report this update" enables automatic reporting for future upgrades
  - "Skip reporting" saves "never ask" preference
- When unchecked, choice applies only to current prompt

Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2026-01-30 08:50:11 +00:00
Damian Schneider f19d29cd64 add json validation to file inputs in UI and minify before upload (#5248)
* also updated edit.htm to do the same
2026-01-30 08:18:17 +01:00
Damian Schneider 1031e70d70 Replace buffer lock magic numbers with defines (#5217)
* replace magic numbers with defines
2026-01-30 08:14:53 +01:00
Frank Möhle c9f47d4b5c new ESP32 node types
Added new node types for unsupported ESP32 variants, based on same file from ESP Easy.

Just to be prepared for new nodes (future support)
2026-01-28 23:37:22 +01:00
Damian Schneider 857e73ab25 adding image rotation to PixelForge gif tool (#5309) 2026-01-28 19:15:28 +01:00
Frank Möhle 4e072962c0 Merge pull request #5323 from wled/ethernet_led_pin
Change default LED pin to 4 in esp32 ethernet builds and set AR default to "no mic" mode.
The "normal" default LED pin 16 is conflicting with pins needed by many ethernet boards, which can cause random crashes.
2026-01-28 15:29:47 +01:00
Frank 81af160be6 disable repeating warning, set all AR pins to "unused"
* ethernet warning was repeating too often
* make sure that AR usermod will not grab any PINs at startup
2026-01-28 15:24:40 +01:00
Frank Möhle a64334c32e correct wrong AR build flag
typo
2026-01-27 23:50:34 +01:00
Frank Möhle 8d39dac654 ethernet: avoid dangerous pins
LED pin: 16 -> 4
AR: no microphone, no pins
clarify comment when to disable ESP-NOW
2026-01-27 23:33:33 +01:00
Frank Möhle e867fcab1a Change default LED pin to 4 in Ethernet builds
GPIO 4 seems to be one of the few pins that is not used in ANY supported ethernet config.

See https://github.com/wled/WLED/issues/5155#issuecomment-3614391561
2026-01-27 23:14:06 +01:00
Frank Möhle 9683896a21 Merge pull request #5317 from gustebeast/wled-gtb-flash-size
Reduce flash size of TetrisAI_V2 by 97%, by removing includes of iostream, iterator and algorithm headers
2026-01-27 17:14:41 +01:00
Martin Fritzsche ca1d6614b2 Add option to save unmodified presets to autosave usermod (#5175)
* Add option to save unmodified presets to autosave usermod

* Fix lastRun never being assigned
2026-01-24 16:26:47 -05:00
gustebeast 96f423438b Reduce flash size of TetrisAI_V2 by 97%
Main branch without Tetris
Flash: [========  ]  79.8% (used 1255301 bytes from 1572864 bytes)

Main branch with Tetris (+196kb)
Flash: [========= ]  92.3% (used 1452049 bytes from 1572864 bytes)

This commit with Tetris (+6kb, 97% less flash)
Flash: [========  ]  80.2% (used 1261625 bytes from 1572864 bytes)
2026-01-24 19:34:32 +00:00
Copilot df94a8d5af Remove MAX_LEDS_PER_BUS limitation for virtual buses (#5238)
- Frontend: Updated settings_leds.htm to allow virtual buses up to 16384 LEDs
- Backend: Modified BusConfig::adjustBounds() to skip MAX_LEDS_PER_BUS check for virtual buses

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: DedeHai <6280424+DedeHai@users.noreply.github.com>
2026-01-23 06:58:27 +01:00
Damian Schneider d9cc751db4 Adding sequential resource loading to edit.htm (#5306)
Avoids multiple parallel connections which is helpful in low heap situations, especially on ESP8266
2026-01-23 06:45:45 +01:00
Damian Schneider 99c3f68f80 Merge pull request #5307 from ChuckMash/main
fix button byte comment
2026-01-22 08:41:07 +01:00
ChuckMash be900737d2 fix button byte comment 2026-01-21 16:25:22 -08:00
Damian Schneider af8db57f02 Fix for cfg exceeding LED limit (#4939)
* Safety Checks for UI, fix for cfg exceeding LED limit
* improvements to low heap check
* add `isPlaceholder()` to bus, some fixes
* remove `disableForceReconnect` for better future implementation
* add "glitch gating" for C3 and check heapy every 5 seconds instead of every secondd
* replace magic number with the correct define, more robust bus defer by look-ahead

In the event that a Bus fails to initialize, or the memory validation
fails, keep the configuration around so the settings contents don't
change out from under the user.

---------

Co-authored-by: Will Miles <will@willmiles.net>
2026-01-19 19:33:06 +01:00
Damian Schneider 1773f61ded bugfix: do not disable "unused" pin type - um_p[] always contains "-1" as a placeholder 2026-01-18 08:37:56 +01:00
Frank Möhle a024935c39 adding a screenshot for simple PR creation
Added a tip for creating pull requests and forking in one click.
2026-01-17 01:53:32 +01:00
Frank Möhle 10df03e564 reorder sections for clarity 2026-01-17 01:39:06 +01:00
Frank Möhle 7a9e7f9c41 Enhance contributing guidelines for pull requests
Added guidelines for creating pull requests from a fork.
2026-01-17 01:37:17 +01:00
Frank Möhle 45acb44a36 Fix typo in CONTRIBUTING.md 2026-01-17 00:53:31 +01:00
Frank Möhle 8a3cb46007 Update contributing guidelines for PR management
Clarify that multiple commits can be added to an open PR and warn against force-pushing.
2026-01-17 00:52:51 +01:00
Frank ba5cf9cd3c night build script updates
Fixes some deprecation warnings during nightly build runs. Already tested in WLED-MM.
* upgrade action-github-changelog-generator to 2.4
* decode changelog into a temporary file (needed for changelog-generator 2.4)
* renamed exclude-labels (deprecated); added some more tags for filtering
2026-01-16 21:47:40 +01:00
Copilot d1d9dec402 Fix gamma correction for color not enabled on fresh install (#5225)
Co-authored-by: DedeHai <6280424+DedeHai@users.noreply.github.com>
2026-01-12 07:55:05 +01:00
Damian Schneider 6e9dc181e1 deepsleep cleanup, use toki to check for valid time 2026-01-10 12:36:07 +01:00
elanworld fe3a158264 Improvements to deepsleep UM (#4456)
- add touch pin option
- add wake-up on timer macro
- fix powerup behaviour: now works as intended i.e. if no sleep delay set, it enters deepsleep on powerup but only then
- apply macro after wake up even if the window is missed
- respect date-range setting of macro
- removed non-existing pin from touch list
- removed non existing voltageCheck setting
- fixed incorrect threshold setting
- code cleanup
- fix pullup/pulldown for deep-sleep GPIOs

pin init is now working correctly, tested on all platforms

* remove unused statement

---------

Co-authored-by: Damian Schneider <daedae@gmx.ch>
2026-01-10 11:28:07 +01:00
Will Tatam e2de1af6f4 Merge pull request #5262 from brettbear/brettbear/ethernet-static-ip-ignored
Fixes ethernet initialisation of static IP settings and modified some debug info
2026-01-10 10:22:44 +00:00
Will Tatam 22ab62d090 Merge pull request #5273 from nomis52/awst
Add support for Australian Western Time
2026-01-07 18:21:47 +00:00
AlexeyMal 254e0099ca Random colors via JSON API in Segment object like "col":["r","r","r"] #4996 (#5000)
Add support for random colors via JSON API in Segment object like col=["r","r","r"] #4996
2026-01-06 22:47:04 -05:00
Frank 8433fd24c3 Add softhack007 to GitHub funding list
adding myself
2026-01-05 23:20:57 +01:00
Frank 32daa03941 Merge pull request #5276 from wled/fix_5275_part1
prevent file data loss due to replacing an open file handle (partial fix for #5275) - provides better protection again presets.json corruption.
2026-01-05 22:23:21 +01:00
Will Tatam 4749247389 Update funding links in FUNDING.yml
Restoring PayPal link for funding. Parity with past contributors
2026-01-05 20:01:36 +00:00
Frank a870474b49 Removed redundant check before closing the file handle 2026-01-05 19:26:39 +01:00
Frank 97493b1af8 Merge pull request #5229 from Merikei/fix-bug-issue_template
bug.yml: Update help text on how to find version description in WLED
2026-01-05 15:18:48 +01:00
Frank 8e27fe4c0c bufgix: prevent file data loss due to replacing an open file pointer
partial fix for #5275
2026-01-05 13:21:37 +01:00
Simon Newton 407c9fd72f Add support for Australian Western Time 2026-01-05 09:13:49 +08:00
Damian Schneider e95450b318 replace cos8 with cos8_t correcting an oversight 2026-01-03 15:45:44 +01:00
Copilot b556da8b36 Bugfix: GPIO0 always gets assigned to a button (#5259)
if button pin arg is missing, it defaults to 0 assigning a button to that pin. This change fixes that.

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: DedeHai <6280424+DedeHai@users.noreply.github.com>
2026-01-03 12:19:56 +01:00
Damian Schneider 5cfb6f984b Palettes fix (#5263)
* Fix for #5201
- use constants instead of magic numbers

Authored-by: Blaž Kristan <blaz@kristan-sp.si>
2026-01-03 10:07:04 +01:00
Damian Schneider 60e1698ed2 Improvements & fixes for HUB75 (#5026)
* Improvements & fixes for HUB75

- added proper config parameters to allow multiple panels
- added config checks
- fixed crashes on S3
- changed constant variables to constexpr
- added "wled.h" to bus_manager.cpp and removed local function prototypes (needed for buffer allocations)
- speed optimisations: yields about 10% higher FPS
- updated platformio_override.sample.ini
- some code cleanup
2026-01-03 09:56:10 +01:00
brettbear 60b2c3bb54 Fixes ethernet initialisation of static IP settings. Also corrected some debug messages. 2026-01-02 18:25:46 +11:00
Will Tatam 979e3fd1f7 Update GitHub sponsors list in FUNDING.yml
Maintain PayPal link, but sponsor is not appropriate for someone who isn't actively contributing going forward
2026-01-01 11:33:26 +00:00
Will Tatam 600b9b4d8e Update funding sources in FUNDING.yml
Removed a contributor from the GitHub funding list and updated the custom funding sources.
2026-01-01 11:29:50 +00:00
Damian Schneider 787d8a7342 fix FX checkmark sync (#5239)
this fixes an ancient copy-paste bug that apparently went under the radar for years. Now FX checkmarks sync correctly.
2025-12-29 14:54:38 +01:00
Damian Schneider 46e77ea203 revert change to extractModeName, add comment for clarification 2025-12-29 14:50:58 +01:00
Damian Schneider fa868568af minor bugfixes as suggested by the rabbit
- PulLightControl UM: dont release a lock you do not own
- off-by-one error in extractModeSlider (used only in rotary in UM)
- safety check in playlist in case something goes horribly wrong
2025-12-29 12:56:06 +01:00
Damian Schneider 1c2cacf185 adding link to WLED VidoLab 2025-12-27 19:27:35 +01:00
Damian Schneider f1f067e93a bugfix in particle collision binning 2025-12-27 09:37:09 +01:00
nename0 8a2a7054ab Usermod Temperature: use full 12-bit precision (#4916)
* Usermod Temperature: use full 12-bit precision

* Temperature Usermod: fix DS1822 and DS18B20 family code

* Add Dropdown to select 9 or 12 bit resolution

* Add 10 and 11 bit resolution, Correct rounding towards negativ inf
2025-12-26 15:32:24 -05:00
Merikei e4b0ee0672 Update version description in bug.yml
Fixes #5228
2025-12-24 23:00:37 +00:00
Damian Schneider b821e20fd6 use constant instead of magic number in pixelforge 2025-12-22 20:06:20 +01:00
Will Miles 51a14ede8b OTA: Report metadata v1 as workaround
Report the older structure version until sufficient penetration
of the previous patch.
2025-12-19 20:44:03 -05:00
Will Miles 788c09972e Remove OTA metadata structure version block
If we cap the acceptable version, it becomes impossible to increase it:
older firmwares will reject it.  Instead we must guarantee
backwards compatibility so long as the magic number is the same.
If a breaking change is required in the future, a new magic number
should be used to identify it.
2025-12-19 20:44:03 -05:00
Will Miles ba21ab55f8 Implement correct update version check 2025-12-19 20:44:03 -05:00
Will Miles 1585cab3ba Add source version check to OTA update
Add a field to the OTA metadata structure indicating the oldest base
version it's safe to install this update /from/.  This provides a clear
path forward in case there are incompatibilities, eg. some case
(bootloader compatibility) where 0.16.0 cannot be installed safely from
0.15.2, but a transitional 0.15.3 can arrange the groundwork.
2025-12-19 20:44:03 -05:00
Will Miles 304c59e09b Revert "Add old version check to OTA update" (#5212) 2025-12-19 20:24:26 -05:00
Blaž Kristan fdb85d82da 2D fix for #5206 2025-12-19 21:01:53 +01:00
Damian Schneider c8a03817ed remove EEPROM support (#5191) 2025-12-19 17:36:40 +01:00
Blaž Kristan dc76ff669b Fix for #5206 2025-12-19 14:15:42 +01:00
Will Tatam eca002b655 Merge pull request #5193 from DedeHai/keep_legacyFX
Do not replace legacy FX with new PS FX
2025-12-19 08:23:24 +00:00
Will Tatam 71c8a30f42 Merge pull request #5057 from willmmiles/update-source-version-check
Add old version check to OTA update
2025-12-19 08:16:19 +00:00
Copilot 65f1d8d836 Fix TypeError when loading UI with custom palette selected (#5205)
* Add null check to fix circular dependency with custom palettes

Co-authored-by: DedeHai <6280424+DedeHai@users.noreply.github.com>

* Refactor: move null check earlier for better efficiency

Co-authored-by: DedeHai <6280424+DedeHai@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: DedeHai <6280424+DedeHai@users.noreply.github.com>
2025-12-18 20:20:15 +01:00
Damian Schneider 624763cbc8 Bugfix in rotary encoder UM: off-by-1 in palette count 2025-12-17 18:49:19 +01:00
Damian Schneider af7c91057e revert gamma change: it was actually correct 2025-12-17 18:47:03 +01:00
Damian Schneider dd3edf1a3c improved 2D collisions, fixed duplicate line in fireworks 1D, remove additional dimming if gamma is active
Adding symmetrical but random pushing leads to better stacking, also pushing more if large particles are too close helps to separate them better. Pushing them every frame also helps.
Pushing only particle1 as it was tends to lead to more collapsing and some random movement within a pile. There was also a bug which applied way to much velocity unnecessarily.
2025-12-16 20:25:24 +01:00
Damian Schneider 7f4e0f74ba nicer random distribution in PS emitter
makes "explosions" in fireworks and impact FX circular instead of square, looking much better.
2025-12-16 08:26:37 +01:00
Frank b452370be7 Merge pull request #5096 from netmindz/no-dmx-ar-conflict
fix for #4298 - no conflict with DMX output
Removes a compile-time error that was blocking compilation when DMX output mode was enabled, allowing successful builds with DMX support.

Important: this fix does not address the output buffer congestion problems that usually make WLED unstable with >100 LEDs on DMX Serial output. It only removes the compile-time blocker in audioreactive.
2025-12-15 21:54:54 +01:00
Damian Schneider 913c7316f2 do not replace legacy FX with new PS FX 2025-12-14 20:18:50 +01:00
BobLoeffler68 bb6114e8aa PacMan effect (#4891)
* PacMan effect added
2025-12-14 12:36:51 +01:00
Will Tatam c097cb1f27 Merge pull request #5185 from wled/copilot/add-repo-field-to-upgradedata
Add repo field to upgradeData in upgrade event reporting
2025-12-14 09:58:20 +00:00
Damian Schneider 9094b3130d Display gaps in peek, fix segment overflow bug (#5105)
- display gaps in peek 
- fix segment overflow bug when loading preset with larger bounds than current setup
2025-12-14 10:26:38 +01:00
Damian Schneider 32b104e1a9 Use sequential loading and requests for all UI resources (#5013)
* use sequential loading for all UI resources

- load common.js and style.css sequentially for all config pages
- restrict all requrests in index.js to single connection
- retry more than once if requests fail
- incremental timeouts to make them faster and still more robust
- bugfix in connectWs()
- on page load, presets are loaded from localStorage if controller was not rebooted
- remove hiding of segment freeze button when not collapsed
2025-12-14 10:13:00 +01:00
Damian Schneider d1260ccf8b clear enable bit on unused time macros (#5134)
disables the checkmark in UI on unused macros
2025-12-14 10:00:28 +01:00
Damian Schneider c35140e763 "WLEDPixelForge": new image & scrolling text interface (#4982)
replaces the pixel magic tool with a much more feature-rich tool for handling gif images. Also adds a scrolling text interface and the possibility to add more tools with a single button click like the classic pixel magic tool and the pixel-painter tool.
2025-12-13 23:49:53 +01:00
copilot-swe-agent[bot] 6632a35339 Remove unnecessary conditional for repo field
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-12-13 22:14:27 +00:00
Damian Schneider 6388b8f4bb Merge pull request #5188 from DedeHai/PS_ellipseRendering
Enhanced particle system rendering & 1D collision handling
- added improved and faster large size rendering to 1D and 2D system
- better size control as size is now exactly mappable to pixels so it can be matched exactly to the collision distance
- no more gaps due to collision distance mismatch
- much faster: saw up to 30% improvement in FPS (2D system)
- adding mass-ratio to collisions for different sized particles
- also adjusted some of the FX to make better use of the new rendering
- added per-particle size rendering to 1D system
- improved and simplified collision handling in 1D system, much more accurate and (almost) no pass-throughs
- removed local blurring functions in PS as they are not needed anymore for particle rendering
- fixed outdated AR handling in PS FX
- fixed infinite loop if not enough memory
- updated PS Hourglass drop interval to simpler math: speed / 10 = time in seconds and improved particle handling
- reduced speed in PS Pinball to fix collision slip-through
- PS Box now auto-adjusts number of particles based on matrix size and particle size
- added safety check to 2D particle rendering to not crash if something goes wrong with out-of bounds particle rendering
- improved binning for particle collisions: dont use binning for small number of particles (faster)
- Some cleanup
2025-12-13 23:14:08 +01:00
Will Tatam f9a8b3021f Merge pull request #4903 from DedeHai/ESPNOW_AP-mode_fix
remove early return from initconnection()
2025-12-13 21:40:38 +00:00
Damian Schneider 6a8c6c1f58 bugfix 2025-12-13 19:54:01 +01:00
Damian Schneider 19bc3c513a lots of tweaks, updated 1D rendering and collisions
- bugfix in mass based 2D collisions
- added improved and faster large size rendering to 1D system
- added per-particle size rendering to 1D system
- improved and simplified collision handling in 1D system
- removed local blurring functions in PS as they are not needed anymore for particle rendering
- adapted FX to work with the new rendering
- fixed outdated AR handling in PS FX
- fixed infinite loop if not enough memory
- updated PS Hourglass drop interval to simpler math: speed / 10 = time in seconds and improved particle handling
- reduced speed in PS Pinball to fix collision slip-through
- PS Box now auto-adjusts number of particles based on matrix size and particle size
- added safety check to 2D particle rendering to not crash if something goes wrong with out-of bounds particle rendering
- improved binning for particle collisions: dont use binning for small number of particles (faster)
- Some code cleanup
2025-12-13 19:05:21 +01:00
Will Tatam fa3a94e33e Merge pull request #5156 from Aogu181/main
Add Gledopto Series With Ethernet
2025-12-13 12:39:11 +00:00
copilot-swe-agent[bot] 5c2177e8d5 Add repo field from info data to upgradeData
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-12-12 10:11:59 +00:00
copilot-swe-agent[bot] 6e39969cdc Initial plan 2025-12-12 10:07:52 +00:00
Aogu181 4b0cf874c9 Add Gledopto Series With Ethernet 2025-12-11 13:38:04 +08:00
Will Tatam 2ff4ee0e1b Merge pull request #5168 from wled/copilot/update-info-endpoint-p-sram
Add psramPresent and psramSize fields to /info endpoint and usage reports
2025-12-10 19:33:47 +00:00
Aogu181 f2a3502445 Add Gledopto Series With Ethernet 2025-12-10 08:43:23 +08:00
copilot-swe-agent[bot] 6ca8ed65e8 Address code review feedback: use boolean coercion instead of logical OR
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-12-09 17:24:04 +00:00
copilot-swe-agent[bot] 6a2b7995e9 Add "Yes, Always" option to upgrade prompt and improve messaging
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-12-09 17:18:23 +00:00
copilot-swe-agent[bot] 247a7a51d7 Initial plan 2025-12-09 17:10:22 +00:00
copilot-swe-agent[bot] 1fee9d4c29 Rename hasPSRAM to psramPresent
- Renamed hasPSRAM field to psramPresent in /info endpoint (json.cpp)
- Updated usage report to use psramPresent field (index.js)
- Removed problematic _codeql_detected_source_root symlink

Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-12-08 19:24:15 +00:00
copilot-swe-agent[bot] b4d3a279e3 Fix potential overflow in PSRAM size calculation
- Changed division from (1024 * 1024) to (1024UL * 1024UL)
- Ensures proper unsigned long type to prevent overflow issues
- All tests pass and firmware builds successfully

Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-12-08 19:05:32 +00:00
copilot-swe-agent[bot] 4684e092a8 Update usage report to send hasPSRAM and psramSize fields
- Modified reportUpgradeEvent in index.js to use new hasPSRAM and psramSize fields
- Changed from calculating psramSize from free PSRAM to using total PSRAM size from /info endpoint
- Both fields now correctly sent in upgrade usage reports to usage.wled.me

Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-12-08 19:01:17 +00:00
copilot-swe-agent[bot] 2a53f415ea Add hasPSRAM and psramSize fields to /info endpoint
- Added hasPSRAM boolean field indicating if hardware has PSRAM
- Added psramSize field with total PSRAM size in MB
- Kept existing psram field for backward compatibility (free PSRAM in bytes)
- All fields only included for ESP32 architecture
- Tests passed and firmware builds successfully

Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-12-08 18:55:26 +00:00
copilot-swe-agent[bot] e074d19593 Initial plan 2025-12-08 18:46:12 +00:00
Aogu181 14a728084c Add Gledopto Series With Ethernet
Add Gledopto Series With Ethernet
2025-12-07 19:44:41 +08:00
Aogu181 ead1d6b5f8 Add Gledopto Series With Ethernet
Add Gledopto Series With Ethernet
2025-12-07 19:43:22 +08:00
Damian Schneider a421cfeabe adding mass-ratio to collisions for different sized particles 2025-12-06 16:31:09 +01:00
Damian Schneider 6f6ac066c9 replaced blur-rendering with ellipse rendering
- better size control as size is now exactly mappable to pixels so it can be matched exactly to the collision distance
- no more gaps due to collision distance mismatch
- much faster: saw up to 30% improvement in FPS
- also adjusted some of the FX to make better use of the new rendering
2025-12-06 14:27:15 +01:00
Aogu181 a55a32cc7e Add Gledopto Series With Ethernet config 2025-12-04 09:03:51 +08:00
Aogu181 474c84c9e6 Add Gledopto Series With Ethernet 2025-12-04 09:02:25 +08:00
Aogu181 a2b64ad332 Merge branch 'wled:main' into main 2025-12-04 08:44:39 +08:00
Frank cc5b504771 Add cherry-picking tip to CONTRIBUTING.md
Added a tip about using cherry-picking for copying commits, especially to copy from a local working branch (e.g. ``main``) to the PR branch.
2025-12-03 20:36:08 +01:00
Frank fe33709eb0 some clarifications
Corrected grammar and clarity in AI contribution guidelines.
2025-12-03 15:19:20 +01:00
Frank 41b51edbdd text styling 2025-12-03 15:15:27 +01:00
Frank e6b5429873 Update CONTRIBUTING.md with AI usage guidelines
Added guidelines for contributions involving AI assistance.
2025-12-03 15:13:38 +01:00
Damian Schneider 8cbc76540f adding dynamic update of LED type dropdown (#5014)
* adding dynamic update of LED type dropdown, remove restriction
* update LED type dropdown upon selection, credit @blazoncek
2025-12-03 06:56:38 +01:00
Aogu181 de01c5e61f Add Gledopto Series With Ethernet 2025-12-03 09:15:32 +08:00
Frank c114ea6b30 AR bugfix
prevents crash in case that audioSource creation (audio startup) failed
2025-12-02 22:57:45 +01:00
Will Tatam bdea3d4959 Merge pull request #5143 from wled/copilot/update-usermods-trigger-logic
Fix usermods.yml to only trigger for external fork PRs
2025-12-02 08:08:14 +00:00
Will Tatam e403f4e0d0 Merge pull request #5147 from willmmiles/npb-894
Replace #5138 with upstream NeoPixelBus fix
2025-12-02 08:05:51 +00:00
Aogu181 3bac2ddae2 Add Gledopto Series With Ethernet 2025-12-02 14:42:10 +08:00
Aogu181 b98b1b4c78 Add Gledopto Series With Ethernet 2025-12-02 14:39:25 +08:00
Aogu181 5885a9cc63 Add Gledopto Series With Ethernet 2025-12-02 14:38:30 +08:00
Aogu181 e306e14b22 Add Gledopto Series With Ethernet 2025-12-02 14:37:16 +08:00
Will Miles 7b9d643dcd Update NeoPixelBus with DMA fix
Includes bonus fix for ESP32 DMA driver, too!
Replaces #5138.
2025-12-01 21:54:04 -05:00
Will Miles f70b359631 Revert "Fix ESP8266 DMA off-by-one"
This reverts commit 4fa4bc8d4b.
2025-12-01 21:43:45 -05:00
Frank ae37f4268c platformio.ini: short hashes => long hashes
short hashes can cause spurious build errors, and will not be supported any more in future platform version.
2025-12-01 20:26:22 +01:00
copilot-swe-agent[bot] 7c6a1d717d Remove push trigger, only run usermods CI for external fork PRs
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-12-01 16:46:13 +00:00
copilot-swe-agent[bot] a2c1ad01da Fix usermods.yml to only run for external fork PRs
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-12-01 16:29:07 +00:00
copilot-swe-agent[bot] a947e8f35e Initial plan 2025-12-01 16:26:35 +00:00
Frank 653e03921e Update repository URLs in package.json
fix outdated URL
2025-12-01 12:55:54 +01:00
Frank a0eec81c8a allow different bootloader sizes for each MCU
not needed yet, but will make maintenance easier in the future, and avoid confusion.
2025-12-01 11:13:18 +01:00
Frank 9eda32b93a add bootloader offsets for -C3, S3, and some future MCU's 2025-12-01 11:11:10 +01:00
Frank e1f5bbf895 avoid #define in generateDeviceFingerprint() 2025-12-01 11:11:09 +01:00
Will Tatam 5d4fdb171e Merge pull request #5138 from willmmiles/esp8266-dma-fix
Fix ESP8266 DMA off-by-one
2025-12-01 08:29:45 +00:00
Will Miles 4fa4bc8d4b Fix ESP8266 DMA off-by-one
Shim in https://github.com/Makuna/NeoPixelBus/pull/894 until approved by
upstream.  Fixes #4906 and #5136.
2025-11-30 22:10:33 -05:00
Damian Schneider b5f13e4331 FIX for adafruit portal S3: remove extra flash section & use default WLED partitions (#5113)
* remove extra flash section, rename board file
* matrixportal: change partitions to standard 8MB

plus minor name adjustment (adding "for WLED")


Co-authored-by: Frank <91616163+softhack007@users.noreply.github.com>
2025-11-30 15:35:25 +01:00
Copilot a897271a03 Convert PSRAM to MB in usage reporting (#5130)
* Initial plan

* Convert PSRAM from bytes to MB in usage reporting JavaScript

Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>

* Use 1024*1024 instead of magic number for bytes to MB conversion

Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-11-28 18:15:58 +00:00
Copilot 75a7ed132a Fix stale UI after firmware updates (#5120)
Add WEB_BUILD_TIME to html_ui.h and use it for ETag generation

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: DedeHai <6280424+DedeHai@users.noreply.github.com>
Co-authored-by: Aircoookie <21045690+Aircoookie@users.noreply.github.com>
2025-11-28 08:08:43 -05:00
Will Miles 379343278d ota_update: Fix NRVO in getBootloaderSHA256Hex
h/t @coderabbitai
2025-11-27 21:46:06 -05:00
Will Miles 0b965ea431 ota_update: Fix hex print 2025-11-27 21:42:57 -05:00
Will Miles d6cd65e108 ota_update: Clean up SHA API
Many of these functions are internal, and we can reduce RAM by caching
the binary value instead.
2025-11-27 20:31:41 -05:00
Will Tatam fc25eb2c90 Merge pull request #5116 from wled/add-report-version-feature
Add report version feature
2025-11-27 22:55:23 +00:00
Will Tatam dc5732a5f5 Merge pull request #5111 from DedeHai/safe_UID_generation
Safe uid generation
2025-11-27 17:59:12 +00:00
Blaž Kristan a9811c2020 Variable button count (up to 32) (#4757)
* Variable button count (up to 32)
- adds ability to configure variable number of buttons during runtime
- fixes #4692
2025-11-27 17:00:58 +01:00
Will Tatam 33411f0300 Reformat to tabs 2025-11-27 13:43:24 +00:00
Will Tatam 8bc434b614 Update working to Aircoookie's suggestion 2025-11-27 13:36:04 +00:00
Damian Schneider ce6577ee35 add caching back 2025-11-27 11:49:33 +01:00
Will Tatam 579021f5fc trigger reportUpgradeEvent 2025-11-26 22:41:45 +00:00
Will Tatam 17e91a7d2a Remove K suffix 2025-11-26 22:11:46 +00:00
Will Tatam 61f5737df2 Remove MB suffix 2025-11-26 22:11:38 +00:00
Will Tatam 49a25af1f2 Fix styling issues 2025-11-26 22:11:26 +00:00
Will Tatam b6f3cb6394 Use deviceId not mac 2025-11-26 22:11:17 +00:00
Will Tatam 571ab674c3 Update to use deviceId 2025-11-26 22:11:02 +00:00
Damian Schneider 6b607fb545 refined PS replacement ifdefs (#5103)
* refined PS replacement ifdefs

* bugfixes, added glitter and sparkle as they a lightweight (1k of flash)
2025-11-26 22:25:10 +01:00
Damian Schneider e761418531 adding legacy support for "edit?list=/" command, fix indentation (#5092) 2025-11-26 22:23:37 +01:00
Damian Schneider fca921ee82 Adding "Complete" mode to Dissolve FX: always fades completely (#5016)
This allows for much slower speed setting to not turn into "twinkle" effect
2025-11-26 22:22:13 +01:00
Damian Schneider fc7993f4a7 update default AP channel to 6, possible fix for "AP does not show" (#5115) 2025-11-26 21:22:22 +01:00
Damian Schneider f12e3e03ac set default AP channel to 7 to help with bad antennas
Channel 1 can have very bad performance on some designs, its better to use a center channel.
2025-11-26 07:33:47 +01:00
Damian Schneider eb87fbf8e4 dont assume initialization of 0, be explicit. 2025-11-25 19:55:25 +01:00
Damian Schneider c534328cc5 return String not uint 2025-11-25 19:45:23 +01:00
Damian Schneider 28d8a1c25c crash-safe version of ID generation using only IDF functions 2025-11-25 19:39:30 +01:00
Will Tatam d1ed5844e3 fix for #4298 - no conflict with DMX output 2025-11-17 17:43:25 +00:00
Will Miles 5bf1fc38b1 Implement correct update version check 2025-11-08 19:18:27 -05:00
Will Miles d55a3f078a Add source version check to OTA update
Add a field to the OTA metadata structure indicating the oldest base
version it's safe to install this update /from/.  This provides a clear
path forward in case there are incompatibilities, eg. some case
(bootloader compatibility) where 0.16.0 cannot be installed safely from
0.15.2, but a transitional 0.15.3 can arrange the groundwork.
2025-11-08 19:00:57 -05:00
Damian Schneider bc5d4fed3c remove early return from initconnection() 2025-09-02 19:46:02 +02:00
197 changed files with 21610 additions and 22913 deletions
+259
View File
@@ -0,0 +1,259 @@
# 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: true
# 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 -2
View File
@@ -1,3 +1,2 @@
github: [Aircoookie,blazoncek,DedeHai,lost-hope,willmmiles]
github: [DedeHai,lost-hope,willmmiles,netmindz,softhack007]
custom: ['https://paypal.me/Aircoookie','https://paypal.me/blazoncek']
thanks_dev: u/gh/netmindz
+6 -1
View File
@@ -44,7 +44,10 @@ body:
id: version
attributes:
label: What version of WLED?
description: You can find this in by going to Config -> Security & Updates -> Scroll to Bottom. Copy and paste the entire line after "Server message"
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>”.
placeholder: "e.g. WLED 0.13.1 (build 2203150)"
validations:
required: true
@@ -60,6 +63,8 @@ body:
- ESP32-S2
- ESP32-C3
- Other
- ESP32-C6 (experimental)
- ESP32-C5 (experimental)
validations:
required: true
- type: textarea
+120
View File
@@ -0,0 +1,120 @@
---
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>` | 1520 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`
+97 -141
View File
@@ -4,169 +4,125 @@ 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.
## Working Effectively
> **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.
### 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)
## Setup
### 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.
- Node.js 20+ (see `.nvmrc`)
- Install dependencies: `npm ci`
- PlatformIO (required only for firmware compilation): `pip install -r requirements.txt`
### 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
## Build and Test
<!-- HUMAN_ONLY_START -->
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`
| 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 | 1520 min |
## Before Finishing Work
<!-- HUMAN_ONLY_END -->
**CRITICAL: You MUST complete ALL of these steps before marking your work as complete:**
- **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`
1. **Run the test suite**: `npm test` -- Set timeout to 2+ minutes. NEVER CANCEL.
- All tests MUST pass
- If tests fail, fix the issue before proceeding
For detailed build timeouts, development workflows, troubleshooting, and validation steps, see [agent-build.instructions.md](agent-build.instructions.md).
2. **Build at least one hardware environment**: `pio run -e esp32dev` -- Set timeout to 30+ minutes. NEVER CANCEL.
- Choose `esp32dev` as it's a common, representative environment
- See "Hardware Compilation" section above for the full list of common environments
- The build MUST complete successfully without errors
- If the build fails, fix the issue before proceeding
- **DO NOT skip this step** - it validates that firmware compiles with your changes
### Usermod Guidelines
3. **For web UI changes only**: Manually test the interface
- See "Manual Testing Scenarios" section below
- Verify the UI loads and functions correctly
- 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`.
**If any of these validation steps fail, you MUST fix the issues before finishing. Do NOT mark work as complete with failing builds or tests.**
## Project Structure Overview
## Validation and Testing
### Project Branch / Release Structure
<!-- HUMAN_ONLY_START -->
### 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
```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 -->
### 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`
- **MANDATORY: Always run a hardware build before finishing** (see "Before Finishing Work" section below)
### 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
## Common Tasks
- ``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
### Repository Structure
```
wled00/ # Main firmware source (C++)
├── data/ # Web interface files
│ ├── index.htm # Main UI
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
│ ├── settings*.htm # Settings pages
│ └── *.js/*.css # Frontend resources
├── *.cpp/*.h # Firmware source files
── html_*.h # Generated embedded web files (DO NOT EDIT)
tools/ # Build tools (Node.js)
│ └── *.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
├── cdata.js # Web UI build script
└── cdata-test.js # Test suite
platformio.ini # Hardware build configuration
package.json # Node.js dependencies and scripts
docs/ # Contributor docs, coding guidelines
.github/workflows/ # CI/CD pipelines
```
### 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
<!-- HUMAN_ONLY_END -->
## General Guidelines
### 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
- **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.
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`
Refer to `docs/cpp.instructions.md` and `docs/web.instructions.md` for language-specific conventions, and `docs/cicd.instructions.md` for GitHub Actions workflows.
3. **For both web and firmware**:
- Always build web UI first
- Test web interface manually
- Build and test firmware if making firmware changes
### Pull Request Expectations
## Build Timing and Timeouts
**IMPORTANT: Use these timeout values when running builds:**
- **Web UI build** (`npm run build`): 3 seconds typical - Set timeout to 30 seconds minimum
- **Test suite** (`npm test`): 40 seconds typical - Set timeout to 120 seconds (2 minutes) minimum
- **Hardware builds** (`pio run -e [target]`): 15-20 minutes typical for first build - Set timeout to 1800 seconds (30 minutes) minimum
- Subsequent builds are faster due to caching
- First builds download toolchains and dependencies which takes significant time
- **NEVER CANCEL long-running builds** - PlatformIO downloads and compilation require patience
**When validating your changes before finishing, you MUST wait for the hardware build to complete successfully. Set the timeout appropriately and be patient.**
## 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 CI workflow will:**
1. Installs Node.js and Python dependencies
2. Runs `npm test` to validate build system (MUST pass)
3. Builds web UI with `npm run build` (automatically run by PlatformIO)
4. Compiles firmware for ALL hardware targets listed in `default_envs` (MUST succeed for all)
5. Uploads build artifacts
**To ensure CI success, you MUST locally:**
- Run `npm test` and ensure it passes
- Run `pio run -e esp32dev` (or another common environment from "Hardware Compilation" section) and ensure it completes successfully
- If either fails locally, it WILL fail in CI
**Match this workflow in your local development to ensure CI success. Do not mark work complete until you have validated builds locally.**
- **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.
+45
View File
@@ -0,0 +1,45 @@
; ----------------------------------------------------------------------------
; 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
+30 -4
View File
@@ -3,7 +3,15 @@ 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:
@@ -11,6 +19,9 @@ 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'
@@ -35,6 +46,9 @@ 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:
@@ -51,8 +65,8 @@ jobs:
~/.platformio/.cache
~/.buildcache
build_output
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') }}-
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') }}-
- name: Set up Python
uses: actions/setup-python@v5
with:
@@ -63,9 +77,21 @@ 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: firmware-${{ matrix.environment }}
name: ${{ steps.artifact_name.outputs.name }}
path: |
build_output/release/*.bin
build_output/release/*_ESP02*.bin.gz
+8 -4
View File
@@ -7,6 +7,9 @@ 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
@@ -23,21 +26,22 @@ jobs:
run: ls -la
- name: "✏️ Generate release changelog"
id: changelog
uses: janheinrichmerker/action-github-changelog-generator@v2.3
uses: janheinrichmerker/action-github-changelog-generator@v2.4
with:
token: ${{ secrets.GITHUB_TOKEN }}
sinceTag: v0.15.0
output: CHANGELOG_NIGHTLY.md
# Exclude issues that were closed without resolution from changelog
exclude-labels: 'stale,wontfix,duplicate,invalid'
excludeLabels: 'stale,wontfix,duplicate,invalid,external,question,use-as-is,not_planned'
- name: Update Nightly Release
uses: andelf/nightly-release@main
uses: andelf/nightly-release@5834076edc55cc05975561c9722043f072ac5c26
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: nightly
name: 'Nightly Release $$'
prerelease: true
body: ${{ steps.changelog.outputs.changelog }}
body_path: CHANGELOG_NIGHTLY.md
files: |
*.bin
*.bin.gz
+16 -6
View File
@@ -5,10 +5,15 @@ on:
tags:
- '*'
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
wled_build:
uses: ./.github/workflows/build.yml
with:
release: true
release:
name: Create Release
@@ -18,21 +23,26 @@ 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.3
uses: janheinrichmerker/action-github-changelog-generator@v2.4
with:
token: ${{ secrets.GITHUB_TOKEN }}
sinceTag: v0.15.0
maxIssues: 500
# Exclude issues that were closed without resolution from changelog
exclude-labels: 'stale,wontfix,duplicate,invalid'
- name: Create draft release
excludeLabels: 'stale,wontfix,duplicate,invalid,external,question,use-as-is,not_planned'
- name: Update release description
uses: softprops/action-gh-release@v1
with:
body: ${{ steps.changelog.outputs.changelog }}
draft: True
files: |
*.bin
*.bin.gz
+8 -5
View File
@@ -1,17 +1,18 @@
name: Usermod CI
on:
push:
paths:
- usermods/**
- .github/workflows/usermods.yml
pull_request:
paths:
- usermods/**
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
get_usermod_envs:
# Only run for pull requests from forks (not from branches within wled/WLED)
if: github.event.pull_request.head.repo.full_name != github.repository
name: Gather Usermods
runs-on: ubuntu-latest
steps:
@@ -31,6 +32,8 @@ jobs:
build:
# Only run for pull requests from forks (not from branches within wled/WLED)
if: github.event.pull_request.head.repo.full_name != github.repository
name: Build Enviornments
runs-on: ubuntu-latest
needs: get_usermod_envs
+6
View File
@@ -10,8 +10,13 @@
compile_commands.json
__pycache__/
/.dummy
/dependencies.lock
/managed_components
esp01-update.sh
platformio_override.ini
platformio_release.ini
replace_fs.py
wled-update.sh
@@ -25,3 +30,4 @@ wled-update.sh
/wled00/Release
/wled00/wled00.ino.cpp
/wled00/html_*.h
/wled00/js_*.h
+267
View File
@@ -0,0 +1,267 @@
# 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) | 30 min |
| `pio run -e nodemcuv2` | Build firmware (ESP8266) | 30 min |
**Always run `npm ci && npm run build` before `pio run`.** The web UI build generates
required C headers for firmware compilation.
### Running a Single Test
Tests use Node.js built-in test runner (`node:test`). The single test file is
`tools/cdata-test.js`. Run it with:
```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 18× slower than DRAM on ESP32 (dual-SPI bus), 310× slower than DRAM on ESP32-S3/-S2 with quad-SPI bus. On ESP32-S3 with octal PSRAM (`CONFIG_SPIRAM_MODE_OCT`), the penalty is smaller (~2×) because the 8-line DTR bus can transfer 8 bits in parallel. On ESP32-P4 with hex PSRAM (`CONFIG_SPIRAM_MODE_HEX`), the 16-line bus runs at 200 MHz which brings it on-par with DRAM.
- Consider that ESP32 often crashes when the largest DRAM chunk gets below 10 KB.
### Preprocessor / Feature Flags
- Feature toggling: `WLED_DISABLE_*` and `WLED_ENABLE_*` flags (exact names matter!)
- `WLED_DISABLE_*`: `2D`, `ADALIGHT`, `ALEXA`, `MQTT`, `OTA`, `INFRARED`, `WEBSOCKETS`, etc.
- `WLED_ENABLE_*`: `DMX`, `GIF`, `HUB75MATRIX`, `JSONLIVE`, `WEBSOCKETS`, etc.
- Platform: `ARDUINO_ARCH_ESP32`, `ESP8266`, `CONFIG_IDF_TARGET_ESP32S3`
### 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.
+175 -28
View File
@@ -1,43 +1,188 @@
## Thank you for making WLED better!
# Thank you for making WLED better!
Here are a few suggestions to make it easier for you to contribute!
WLED is a community-driven project, and every contribution matters! We appreciate your time and effort.
### Describe your PR
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!
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.
## Getting Started
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 😉)
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" />
### Target branch for pull requests
Please make all PRs against the `main` branch.
> [!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.
### Updating your code
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.
While the PR is open, you can keep updating your branch - just push more commits! GitHub will automatically update your PR.
> [!CAUTION]
You don't need to squash commits or clean up history - we'll handle that when merging.
> [!CAUTION]
> Do not use "force-push" while your PR is open!
> 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.
> 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.
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
### Responding to Reviews
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
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.
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:
#### 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
@@ -55,7 +200,7 @@ if (a == b) {
if (a == b) doStuff(a);
```
Acceptable - however the first variant is usually easier to read:
Also acceptable (though the first style is usually easier to read):
```cpp
if (a == b)
{
@@ -86,20 +231,22 @@ if( a==b ){
#### Comments
Comments should have a space between the delimiting characters (e.g. `//`) and the comment text.
Note: This is a recent change, the majority of the codebase still has comments without spaces.
We're gradually adopting this style - don't worry if you see older code without spaces!
Good:
```
// This is a comment.
/* This is a CSS inline comment */
```cpp
// This is a short inline comment.
/*
* This is a comment
* This is a longer 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 -->
```
@@ -2,7 +2,7 @@
"build": {
"arduino":{
"ldscript": "esp32s3_out.ld",
"partitions": "partitions-8MB-tinyuf2.csv"
"partitions": "default_8MB.csv"
},
"core": "esp32",
"extra_flags": [
@@ -43,16 +43,8 @@
"arduino",
"espidf"
],
"name": "Adafruit MatrixPortal ESP32-S3",
"name": "Adafruit MatrixPortal ESP32-S3 for WLED",
"upload": {
"arduino": {
"flash_extra_images": [
[
"0x410000",
"variants/adafruit_matrixportal_esp32s3/tinyuf2.bin"
]
]
},
"flash_size": "8MB",
"maximum_ram_size": 327680,
"maximum_size": 8388608,
+162
View File
@@ -0,0 +1,162 @@
---
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
+528
View File
@@ -0,0 +1,528 @@
---
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 (0255)
* @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 28 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 (24).
### 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)
```
+859
View File
@@ -0,0 +1,859 @@
---
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 3961). 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 28 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 3337 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 3337 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.44.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.44.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.15.3 | **13.2** | C++20 (gnu++2b) | Significant warning changes |
| 5.45.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) | 200320 KB | Hot-path buffers, task stacks, small allocations |
| IRAM | `MALLOC_CAP_EXEC` | Fastest | No | 32128 KB | Code (automatic via `IRAM_ATTR`) |
| PSRAM (SPIRAM) | `MALLOC_CAP_SPIRAM \| MALLOC_CAP_8BIT` | Slower | Chip-dependent | 216 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, 310× 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 1314 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 15003000 µ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 -->
+58
View File
@@ -0,0 +1,58 @@
---
applyTo: "**/*.{cpp,h,hpp,ino,js,htm,html,css,yml,yaml}"
description: "WLED strict-mode security review: low-noise checklist."
---
# WLED Security Review — Low Noise Mode
Use these code hardening rules for automated reviews with minimal false positives.
## WLED Constraints (apply to all rules)
- Assume firewall/DMZ/VPN deployment; focus on realistic LAN-local and supply-chain risks.
- Do **not** require TLS/HTTPS as a baseline control.
- Do **not** require authentication for standards-based UDP multicast/broadcast paths where authentication is not defined in the protocol specification.
> **Trust boundary model**: Apply input-validation rules **only at the first untrusted ingress point**
> (HTTP/JSON API body or query string, WebSocket payload, UDP datagram, TCP read, serial command, ESP-NOW raw messages).
> Values that have been validated and range-clamped at ingress are **trusted** for internal WLED
> processing. Do not flag subsequent uses or internal copies of already-sanitized data.
## CRITICAL Rules
1. **No unchecked buffer copies** (`memcpy`, `memmove`, `strcpy`) in firmware paths when source buffer or size comes from an untrusted origin; prefer bounded alternatives (`strncpy`, `strlcpy`); require length validation before copying.
2. **No user-controlled format strings** in `DEBUG_PRINTF*` and similar logging APIs.
3. **Validate all untrusted external input** (HTTP/JSON/UDP/serial) before index/length/pin usage.
4. **Auth required for state-changing control endpoints where feasible** (for example HTTP/JSON); do not flag protocol-defined unauthenticated UDP multicast/broadcast channels solely for missing auth.
5. **No fail-open on parse/allocation errors** for config/state updates.
6. **No DOM XSS sinks with untrusted data** (`innerHTML`, unsafe HTML insertion). Server-side generation of JavaScript property-assignment statements (as used in WLED's printSetForm* helpers) is exempt.
7. **No dynamic code execution** (`eval`, `new Function`, string timers).
8. **No hardcoded secrets/credentials/tokens/keys** in committed files.
9. **No sensitive data in logs** (passwords, tokens, Wi-Fi secrets, auth headers).
10. **No secret exposure in workflows/log output, or in LittleFS files other than `wsec.json`**.
11. **No unsafe third-party GitHub Action pinning** (`@main`/`@master` disallowed).
12. **No untrusted expression interpolation in workflow shell commands**.
## IMPORTANT Rules
13. Avoid potentially unbounded string/memory operations (`strcmp`, `strchr`, `strlen`, `sprintf`) in firmware paths; prefer bounded alternatives (`strnlen`, `strncmp`, `snprintf`).
14. Check integer overflow risks in size/index arithmetic, but consider that unsigned wrap-around on small types might be intentional.
15. Reject repeated heap allocation churn in hot render/effect loops.
16. Avoid repeated `String` growth in hot paths; prefer bounded/pre-allocated buffers.
17. Ensure UI validation is mirrored by firmware-side validation.
18. Require strict origin checks for `postMessage` listeners.
19. Disallow untrusted redirect/navigation targets.
20. Prevent verbose error responses that leak internals.
21. Review new dependencies for typosquatting and known vulnerability risk.
22. Keep workflow `permissions` least-privilege.
23. Verify new `WLED_ENABLE_*` / `WLED_DISABLE_*` names are valid known flags.
24. New privileged behavior must not be enabled by insecure defaults; first-use default-credential change required where applicable.
25. OTA paths (Update.begin(), Update.write()) must verify firmware integrity (checksum/hash); TLS not required.
26. Flag xTaskCreate/xTaskCreatePinnedToCore tasks with insufficient stack for String/JSON use; flag MDNS.begin() / ArduinoOTA.setHostname() with unsanitized hostnames.
27. Flag API/config serialization that exposes Wi-Fi/AP/MQTT password fields to unauthenticated clients.
28. Treat fetched and config-derived strings as untrusted when inserting into the DOM; explicit sanitization required for HTML contexts.
## Reviewer Output Format
- Include severity, exact file and line, and one concrete fix direction.
- Prioritize CRITICAL findings before IMPORTANT findings.
+227
View File
@@ -0,0 +1,227 @@
---
applyTo: "**/*.{cpp,h,hpp,ino,js,htm,html,css,yml,yaml}"
description: "WLED-focused security review guide based on OWASP Top 10 for embedded firmware and web UI."
---
# WLED Security Review Standards (Embedded + Web UI)
Use this guide for AI-assisted code reviews in:
- `wled00/`
- `usermods/`
- `.github/workflows/`
## WLED Constraints and Threat Model Assumptions
- Assume typical deployment behind a firewall/DMZ/VPN; prioritize LAN-local and supply-chain risks.
- Do **not** require TLS/HTTPS as a baseline control for findings in this repo.
- Do **not** require authentication for standards-based UDP multicast/broadcast protocols where auth is not part of the spec.
- Do not propose mitigations that break protocol compliance just to add authentication.
### Trust Boundary Model
**Untrusted data** enters WLED only at the following explicit ingress points:
- HTTP/JSON API request bodies and query parameters (e.g., `/json/*`, `/win`)
- WebSocket message payloads
- UDP datagrams (`parsePacket()` / `recvfrom()` and higher-level protocol wrappers)
- TCP socket reads
- Serial/UART input used as commands
- ESP-NOW raw messages input
**Validation and range-clamping applied at the ingress point renders data trusted** for all subsequent use within the WLED core.
**Do not flag:**
- Repeated bounds or range checks on a value that has already been validated and clamped at its ingress handler.
- Internal WLED core logic that operates on values confirmed safe by the ingress layer.
If it is unclear whether a value has been sanitized upstream (e.g., passed through multiple function calls without a clear annotation), prefer asking for clarification over raising a false-positive finding.
### Locally-Stored Configuration Files (Robustness, not a primary trust boundary)
Files read from LittleFS (`presets.json`, `cfg.json`, `ledmap.json`, `ir.json`, etc.) are written only via privileged access (`/edit`) and are considered trusted in the threat model.
However, parse them defensively (validate structure, clamp array sizes, handle missing keys gracefully) to avoid bootloops from filesystem corruption or accidental malformation.
## Severity
- **CRITICAL** — exploitable vulnerability; block merge.
- **IMPORTANT** — meaningful risk; fix before or with merge when practical.
- **SUGGESTION** — defense-in-depth; track for follow-up.
## Scope (WLED-relevant)
Prioritize:
- C++ memory safety and input validation
- Auth and access checks for state-changing HTTP/JSON APIs
- XSS and DOM safety in `wled00/data/*`
- Secrets handling (`wsec.json`) and secure logging
- Dependency and GitHub Actions supply-chain hygiene
- Fail-safe behavior on constrained devices
De-prioritize unless explicitly introduced by a PR:
- SQL/NoSQL checks, JWT/OAuth flows, GraphQL-specific checks, generic backend framework checks not used by WLED.
## Firmware Security (C++, OWASP A01/A04/A05/A10)
### FW1: Unsafe buffer operations
- **Severity**: CRITICAL
- Flag `strcpy`, `sprintf`, unchecked memory access (`memcpy`, `memmove`, `memcmp`, `strcmp`, `strlen`), unchecked pointer arithmetic.
- Require explicit bounds checks and length validation.
- Prefer bounded alternatives for string operations (`strnlen`, `strncmp`, `strncpy`, `strlcpy`, `snprintf`).
- Treat a finding against FW1 as **suggestion** only when the operation is provably bounded
and both the destination capacity and copied/compared length are known safe.
### FW2: Format-string injection
- **Severity**: CRITICAL
- Do not pass untrusted input as a format string to `DEBUG_PRINTF*` or similar APIs.
### FW3: Integer overflow in length and offset math
- **Severity**: IMPORTANT
- Review `count * size`, index math, narrowing casts before allocations or copies.
### FW4: Unvalidated external input
- **Severity**: CRITICAL
- At each **untrusted ingress point** (see Trust Boundary Model above), validate and clamp values from HTTP/JSON/UDP/serial before use as lengths, indices, IDs, or pin references.
- Do not flag repeated range checks on values that have already been validated at their ingress point.
- In UDP handlers (`parsePacket()`, `read()`, and any lower-level socket wrappers), validate `packetSize` before buffer writes and clamp protocol-specific universe/channel ranges to valid limits.
### FW5: Missing auth checks on state-changing endpoints (where auth is feasible)
- **Severity**: CRITICAL
- HTTP/JSON and other control paths that support auth must enforce configured auth policy.
- Do not flag the HTTP endpoint `/reset` as state-changing. This endpoint triggers a reboot, causing a short interruption without loss of user data.
- Do not flag standards-based UDP multicast/broadcast paths solely for lacking authentication when authentication is not defined in the protocol specification.
### FW6: Fail-open behavior after parse or allocation errors
- **Severity**: IMPORTANT
- On error, reject update and preserve safe previous state.
- Explicitly check parse status (`DeserializationError error = deserializeJson(...); if (error) return/reject;`) and avoid silently applying unsafe zero/default values to safety-relevant fields (for example LED count and pin assignment).
### FW7: Heap churn in hot paths
- **Severity**: IMPORTANT
- Avoid repeated dynamic allocation in render/effect loops; prefer pre-allocation and reuse.
- Flag allocation patterns in loop and ISR-adjacent paths that can trigger fragmentation or timing instability.
### FW8: Unsafe use of `String` in performance-critical paths
- **Severity**: IMPORTANT
- In hot paths, avoid repeated `String` growth; reserve or use fixed buffers.
- Flag repeated `String` concatenation inside loop-heavy or ISR-adjacent code.
### FW9: Unsafe feature flag names
- **Severity**: IMPORTANT
- Verify all new `WLED_ENABLE_*`/`WLED_DISABLE_*` names are valid known flags; typos silently alter build behavior.
### FW10: OTA integrity verification (without TLS requirement)
- **Severity**: IMPORTANT
- OTA update flows should validate firmware integrity using the checksum/hash/signature mechanism available in the firmware/platform implementation.
- Do not require TLS/certificate pinning as a mandatory review criterion.
- In OTA paths (`Update.begin()`, `Update.write()`, and related flows), flag flashing without integrity verification.
### FW11: FreeRTOS task stack and recursion safety
- **Severity**: IMPORTANT
- In `xTaskCreate`/`xTaskCreatePinnedToCore` tasks that process `String`/JSON-heavy data, verify stack-size sufficiency and avoid unbounded recursion.
### FW12: mDNS and hostname sanitization
- **Severity**: IMPORTANT
- For `MDNS.begin()`, `MDNS.addService()`, and `ArduinoOTA.setHostname()`, ensure user-provided hostnames are RFC-compliant (letters/digits/hyphen, no leading/trailing hyphen) and clamped to 63 characters.
### FW13: Outbound URL validation (no HTTPS requirement)
- **Severity**: SUGGESTION
- When using user-provided URL strings with `HTTPClient.begin()`/equivalent, validate scheme/format and constrain host targets (allowlist or equivalent policy).
- Do not require HTTPS/TLS as a baseline review rule.
### FW14: Optional unicast UDP source filtering
- **Severity**: SUGGESTION
- For unicast UDP receive paths, prefer optional user-configurable source filtering.
- Do not require this for multicast/broadcast protocol flows.
## Web UI Security (`wled00/data/*`, OWASP A01/A02/A05)
### WEB1: DOM XSS through `innerHTML`
- **Severity**: CRITICAL
- Prefer `textContent`; if HTML is required, sanitize trusted content path explicitly.
### WEB2: Dynamic code execution
- **Severity**: CRITICAL
- Reject `eval`, `new Function`, and string-based timer execution.
### WEB3: `postMessage` without origin validation
- **Severity**: IMPORTANT
- Require strict origin allowlist checks before processing payloads.
### WEB4: Unsafe redirects/navigation
- **Severity**: IMPORTANT
- Do not navigate directly from untrusted query/input without relative-path or allowlist checks.
### WEB5: Client-only validation
- **Severity**: IMPORTANT
- UI validation is not sufficient; equivalent firmware-side validation is required.
### WEB6: Direct DOM insertion from fetched/config data
- **Severity**: IMPORTANT
- Treat fetched and config-derived strings as untrusted unless proven otherwise.
### WEB7: CSRF checks for state-changing HTTP routes (advisory)
- **Severity**: SUGGESTION
- For state-changing HTTP routes (for example `/json/state`, `/win`), prefer `Origin`/`Referer` header validation as low-cost defense-in-depth for deployments that are not directly internet-exposed.
- Treat this as advisory only, since some legitimate clients may omit these headers.
## Secrets and Logging (OWASP A04/A09/A10)
### SEC1: Hardcoded secrets and credentials
- **Severity**: CRITICAL
- Reject committed API keys, passwords, tokens, private keys, or test backdoors with potential security impact.
### SEC2: Sensitive values in logs
- **Severity**: CRITICAL
- Do not log passwords, tokens, Wi-Fi keys, auth headers, or full sensitive payloads.
### SEC3: Insecure defaults
- **Severity**: IMPORTANT
- Reject new default credentials or insecure auto-enable behavior for privileged functions.
- For setup/onboarding flows, require first-change behavior for default credentials where applicable.
### SEC4: Overly detailed error responses
- **Severity**: IMPORTANT
- Avoid exposing stack traces or internal details to API/UI consumers.
### SEC5: Credential exposure in API/config responses
- **Severity**: IMPORTANT
- Flag API/config serialization that exposes password-like fields (for example Wi-Fi/AP/MQTT passwords) to unauthenticated or untrusted clients.
### SEC6: Security-relevant event logging coverage
- **Severity**: SUGGESTION
- Prefer explicit logging for auth failures, OTA attempts, config resets, and AP activation events, without logging secret values.
## Supply Chain and CI/CD (OWASP A03/A08)
### SC1: New dependency risk
- **Severity**: IMPORTANT
- Review new npm/pip/PlatformIO dependencies for legitimacy, pinning, and known vulnerabilities.
### SC2: Workflow permission hardening regressions
- **Severity**: IMPORTANT
- Check for broad `permissions`, unpinned third-party actions, or unsafe secret exposure.
- Flag mutable third-party action refs (`@main`, `@master`, broad tags) where SHA pinning is expected by project policy.
- Flag overly broad permissions such as `write-all` without clear need.
### SC3: Script injection in workflows
- **Severity**: IMPORTANT
- Avoid direct interpolation of untrusted `${{ github.event.* }}` values in `run` commands.
## Reviewer Checklist
- [ ] No new memory-safety hazards (bounds, overflow, unsafe copies/format strings)
- [ ] External input is validated and range-clamped at ingress points (HTTP/JSON, WebSocket, UDP, TCP, serial, ESP-NOW)
- [ ] State-changing API paths enforce auth policy
- [ ] OTA paths enforce integrity verification (without requiring TLS baseline)
- [ ] Suggested rule patterns are checked where relevant (UDP bounds, hostname sanitization, workflow pinning/permissions)
- [ ] Web UI changes avoid unsafe DOM execution/injection patterns
- [ ] No secrets added; no sensitive logging introduced
- [ ] Error handling remains fail-safe and non-leaky
- [ ] Dependency/workflow changes are supply-chain safe
- [ ] Feature-flag names are valid and not typoed
## AI Review Behavior
- Prefer concrete, file/line-specific findings over generic guidance.
- Prioritize **CRITICAL** and **IMPORTANT** findings.
- Skip irrelevant framework checks not used by WLED.
- If control-flow trust is unclear, ask for clarification instead of guessing.
+36
View File
@@ -0,0 +1,36 @@
---
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.**
+30 -28
View File
@@ -1,17 +1,17 @@
{
"name": "wled",
"version": "0.16.0-alpha",
"version": "16.0.1-rc1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "wled",
"version": "0.16.0-alpha",
"version": "16.0.1-rc1",
"license": "ISC",
"dependencies": {
"clean-css": "^5.3.3",
"html-minifier-terser": "^7.2.0",
"nodemon": "^3.1.9",
"nodemon": "^3.1.14",
"web-resource-inliner": "^7.0.0"
},
"engines": {
@@ -111,10 +111,13 @@
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"license": "MIT"
"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"
}
},
"node_modules/binary-extensions": {
"version": "2.3.0",
@@ -129,13 +132,15 @@
}
},
"node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz",
"integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
"balanced-match": "^4.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
}
},
"node_modules/braces": {
@@ -211,12 +216,6 @@
"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",
@@ -524,15 +523,18 @@
}
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"license": "ISC",
"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",
"dependencies": {
"brace-expansion": "^1.1.7"
"brace-expansion": "^5.0.2"
},
"engines": {
"node": "*"
"node": "18 || 20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/ms": {
@@ -552,15 +554,15 @@
}
},
"node_modules/nodemon": {
"version": "3.1.9",
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.9.tgz",
"integrity": "sha512-hdr1oIb2p6ZSxu3PB2JWWYS7ZQ0qvaZsc3hK8DR8f02kRzc8rjYmxAIvdz+aYC+8F2IjNaB7HMcSDg8nQpJxyg==",
"version": "3.1.14",
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz",
"integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==",
"license": "MIT",
"dependencies": {
"chokidar": "^3.5.2",
"debug": "^4",
"ignore-by-default": "^1.0.1",
"minimatch": "^3.1.2",
"minimatch": "^10.2.1",
"pstree.remy": "^1.1.8",
"semver": "^7.5.3",
"simple-update-notifier": "^2.0.0",
+6 -6
View File
@@ -1,6 +1,6 @@
{
"name": "wled",
"version": "0.16.0-alpha",
"version": "16.0.1-rc1",
"description": "Tools for WLED project",
"main": "tools/cdata.js",
"directories": {
@@ -14,21 +14,21 @@
},
"repository": {
"type": "git",
"url": "git+https://github.com/wled-dev/WLED.git"
"url": "git+https://github.com/wled/WLED.git"
},
"author": "",
"license": "ISC",
"bugs": {
"url": "https://github.com/wled-dev/WLED/issues"
"url": "https://github.com/wled/WLED/issues"
},
"homepage": "https://github.com/wled-dev/WLED#readme",
"homepage": "https://github.com/wled/WLED#readme",
"dependencies": {
"clean-css": "^5.3.3",
"html-minifier-terser": "^7.2.0",
"web-resource-inliner": "^7.0.0",
"nodemon": "^3.1.9"
"nodemon": "^3.1.14"
},
"engines": {
"node": ">=20.0.0"
}
}
}
+80
View File
@@ -0,0 +1,80 @@
# 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")
from pathlib import Path
# Linker script fragment injected into the rodata output section of whichever
# platform we're building for. Placed just before the end-of-rodata marker so
# that the dynarray entries land in flash rodata and are correctly sorted.
DYNARRAY_INJECTION = (
"\n /* dynarray: WLED dynamic module arrays */\n"
" . = ALIGN(0x10);\n"
" KEEP(*(SORT_BY_INIT_PRIORITY(.dynarray.*)))\n"
" "
)
def inject_before_marker(path, marker):
"""Patch a linker script file in-place, inserting DYNARRAY_INJECTION before marker."""
original = path.read_text()
path.write_text(original.replace(marker, DYNARRAY_INJECTION + marker, 1))
if env.get("PIOPLATFORM") == "espressif32":
# Find sections.ld on the linker search path (LIBPATH).
sections_ld_path = None
for ld_dir in env.get("LIBPATH", []):
candidate = Path(str(ld_dir)) / "sections.ld"
if candidate.exists():
sections_ld_path = candidate
break
if sections_ld_path is not None:
# Inject inside the existing .flash.rodata output section, just before
# _rodata_end. IDF v5 enforces zero gaps between adjacent output
# sections via ASSERT statements, so INSERT AFTER .flash.rodata would
# fail. Injecting inside the section creates no new output section and
# leaves the ASSERTs satisfied.
build_dir = Path(env.subst("$BUILD_DIR"))
patched_path = build_dir / "dynarray_sections.ld"
import shutil
shutil.copy(sections_ld_path, patched_path)
inject_before_marker(patched_path, "_rodata_end = ABSOLUTE(.);")
# Replace "sections.ld" in LINKFLAGS with an absolute path to our
# patched copy. The flag may appear as a bare token, combined as
# "-Tsections.ld", or split across two tokens ("-T", "sections.ld").
patched_str = str(patched_path)
new_flags = []
skip_next = False
for flag in env.get("LINKFLAGS", []):
if skip_next:
new_flags.append(patched_str if flag == "sections.ld" else flag)
skip_next = False
elif flag == "-T":
new_flags.append(flag)
skip_next = True
else:
new_flags.append(flag.replace("sections.ld", patched_str))
env.Replace(LINKFLAGS=new_flags)
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)
+121 -18
View File
@@ -1,6 +1,8 @@
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
@@ -25,25 +27,117 @@ def find_usermod(mod: str) -> Path:
return mp
raise RuntimeError(f"Couldn't locate module {mod} in usermods directory!")
def is_wled_module(dep: LibBuilderBase) -> bool:
"""Returns true if the specified library is a wled module
# 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.
"""
return usermod_dir in Path(dep.src_dir).parents or str(dep.name).startswith("wled-")
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
## Script starts here
# Process usermod option
usermods = env.GetProjectOption("custom_usermods","")
# 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()
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
)
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)
## Script starts here — parse custom_usermods
raw_usermods = env.GetProjectOption("custom_usermods", "")
usermods_libdeps: list[str] = []
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)
# Utility function for assembling usermod include paths
def cached_add_includes(dep, dep_cache: set, includes: deque):
@@ -86,6 +180,14 @@ 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)
@@ -93,9 +195,10 @@ 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)} -- modules will not compile in correctly",
fg="red",
err=True)
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)
Exit(1)
# Save the depbuilders list for later validation
+119 -29
View File
@@ -1,16 +1,11 @@
import re
from pathlib import Path # For OS-agnostic path manipulation
from typing import Iterable
import subprocess
from pathlib import Path
from click import secho
from SCons.Script import Action, Exit
from platformio.builder.tools.piolib import LibBuilderBase
Import("env")
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-")
_ATTR = re.compile(r'\bDW_AT_(name|comp_dir)\b')
def read_lines(p: Path):
@@ -19,35 +14,129 @@ def read_lines(p: Path):
return f.readlines()
def check_map_file_objects(map_file: list[str], dirs: Iterable[str]) -> set[str]:
""" Identify which dirs contributed to the final build
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)))
Returns the (sub)set of dirs that are found in the output ELF
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.
"""
# 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")
readelf_path = _get_readelf_path(env)
secho(f"INFO: Checking for usermod compilation units...")
try:
result = subprocess.run(
[readelf_path, "--debug-dump=info", "--dwarf-depth=1", str(elf_path)],
capture_output=True, text=True, errors="ignore", timeout=120,
)
output = result.stdout
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
remaining = {Path(str(b.src_dir)): Path(b.build_dir).name for b in module_lib_builders}
found = set()
for line in map_file:
matches = object_path_regex.findall(line)
for m in matches:
found.add(m)
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
return found
def count_usermod_objects(map_file: list[str]) -> int:
""" 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])
""" 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
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)
@@ -65,16 +154,17 @@ def validate_map_file(source, target, env):
usermod_object_count = count_usermod_objects(map_file_contents)
secho(f"INFO: {usermod_object_count} usermod object entries")
confirmed_modules = check_map_file_objects(map_file_contents, modules.keys())
elf_path = build_dir / env.subst("${PROGNAME}.elf")
confirmed_modules = check_elf_modules(elf_path, env, module_lib_builders)
missing_modules = [modname for mdir, modname in modules.items() if mdir not in confirmed_modules]
if missing_modules:
secho(
f"ERROR: No object files from {missing_modules} found in linked output!",
f"ERROR: No symbols 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'))
+183 -8
View File
@@ -29,6 +29,8 @@ default_envs = nodemcuv2
esp32S3_wroom2
esp32s3dev_16MB_opi
esp32s3dev_8MB_opi
esp32s3dev_8MB_qspi
esp32s3dev_8MB_none
esp32s3_4M_qspi
usermods
@@ -37,6 +39,7 @@ data_dir = ./wled00/data
build_cache_dir = ~/.buildcache
extra_configs =
platformio_override.ini
platformio_release.ini
[common]
# ------------------------------------------------------------------------------
@@ -120,6 +123,7 @@ 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 =
@@ -133,6 +137,7 @@ extra_scripts =
pre:pio-scripts/set_metadata.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
@@ -158,9 +163,8 @@ upload_speed = 115200
# ------------------------------------------------------------------------------
lib_compat_mode = strict
lib_deps =
fastled/FastLED @ 3.6.0
IRremoteESP8266 @ 2.8.2
makuna/NeoPixelBus @ 2.8.3
https://github.com/Makuna/NeoPixelBus.git#a0919d1c10696614625978dd6fb750a1317a14ce
https://github.com/Aircoookie/ESPAsyncWebServer.git#v2.4.2
marvinroger/AsyncMqttClient @ 0.9.0
# for I2C interface
@@ -191,6 +195,7 @@ extra_scripts = ${scripts_defaults.extra_scripts}
[esp8266]
build_unflags = ${common.build_unflags}
custom_usermods =
build_flags =
-DESP8266
-DFP_IN_IROM
@@ -219,6 +224,7 @@ lib_deps =
ESPAsyncTCP @ 1.2.2
ESPAsyncUDP
ESP8266PWM
https://github.com/tignioj/ArduinoUZlib.git#20aff95cd80c141f80bdbf66895409a0046d2c2f
${env.lib_deps}
;; compatibilty flags - same as 0.14.0 which seems to work better on some 8266 boards. Not using PIO_FRAMEWORK_ARDUINO_MMU_CACHE16_IRAM48
@@ -246,17 +252,17 @@ 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.7
bitbank2/AnimatedGIF@^1.4.7
https://github.com/Aircoookie/GifDecoder#bc3af18
https://github.com/Aircoookie/GifDecoder.git#bc3af189b6b1e06946569f6b4287f0b79a860f8e
build_flags =
-D CONFIG_ASYNC_TCP_USE_WDT=0
-D CONFIG_ASYNC_TCP_STACK_SIZE=8192
@@ -289,9 +295,18 @@ AR_lib_deps = ;; for pre-usermod-library platformio_override compatibility
;; please note that you can NOT update existing ESP32 installs with a "V4" build. Also updating by OTA will not work properly.
;; You need to completely erase your device (esptool erase_flash) first, then install the "V4" build from VSCode+platformio.
;; 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
;; tasmota platform (default)
platform = https://github.com/tasmota/platform-espressif32/releases/download/2024.06.00/platform-espressif32.zip ;; Tasmota Arduino Core 2.0.18 with IPv6 support, based on IDF 4.4.8
platform_packages =
;; espressif platform (optional - needs 300KB extra flash)
;;; arduino-esp32 2.0.17 + esp-idf 4.4.7
;; platform = espressif32@ ~6.13.0
;; platform_packages =
;;; arduino-esp32 2.0.14 + esp-idf 4.4.6
;; platform = espressif32@ ~6.6.0
;; platform_packages = platformio/framework-arduinoespressif32 @ 3.20014.231204
build_unflags = ${common.build_unflags}
build_flags = -g
-Wshadow=compatible-local ;; emit warning in case a local variable "shadows" another local one
@@ -300,7 +315,7 @@ build_flags = -g
-D WLED_ENABLE_DMX_INPUT
lib_deps =
${esp32_all_variants.lib_deps}
https://github.com/someweisguy/esp_dmx.git#47db25d
https://github.com/someweisguy/esp_dmx.git#47db25d8c515e76fabcf5fc5ab0b786f98eeade0
${env.lib_deps}
[esp32s2]
@@ -375,6 +390,7 @@ build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=
-D WLED_DISABLE_PARTICLESYSTEM2D
lib_deps = ${esp8266.lib_deps}
monitor_filters = esp8266_exception_decoder
custom_usermods = audioreactive
[env:nodemcuv2_compat]
extends = env:nodemcuv2
@@ -384,6 +400,7 @@ 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
@@ -402,6 +419,7 @@ build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=
-D WLED_DISABLE_PARTICLESYSTEM2D
-D WLED_DISABLE_PARTICLESYSTEM1D
lib_deps = ${esp8266.lib_deps}
custom_usermods = audioreactive
[env:esp8266_2m_compat]
extends = env:esp8266_2m
@@ -411,6 +429,7 @@ 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
@@ -420,6 +439,28 @@ build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=
-D WLED_DISABLE_PARTICLESYSTEM2D
custom_usermods = audioreactive
[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 = env:esp8266_2m
board = esp_wroom_02
board_build.ldscript = ${common.ldscript_2m512k}
build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP02_min\"
-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
[env:esp01_1m_full]
board = esp01_1m
platform = ${common.platform_wled_default}
@@ -430,6 +471,7 @@ 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
lib_deps = ${esp8266.lib_deps}
[env:esp01_1m_full_compat]
@@ -440,6 +482,7 @@ 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
@@ -448,6 +491,7 @@ 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
[env:esp32dev]
@@ -510,8 +554,10 @@ upload_speed = 921600
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 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
; -D WLED_DISABLE_ESPNOW ;; ESP-NOW requires wifi, may crash with ethernet only
lib_deps = ${esp32.lib_deps}
board_build.partitions = ${esp32.default_partitions}
board_build.flash_mode = dio
@@ -537,6 +583,7 @@ platform_packages = ${esp32c3.platform_packages}
framework = arduino
board = esp32-c3-devkitm-1
board_build.partitions = ${esp32.default_partitions}
custom_usermods = audioreactive
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
@@ -594,6 +641,32 @@ board_build.f_flash = 80000000L
board_build.flash_mode = qio
monitor_filters = esp32_exception_decoder
[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
monitor_filters = esp32_exception_decoder
[env:esp32s3dev_8MB_none]
;; ESP32-S3 development board, 8MB FLASH, no PSRAM
extends = esp32s3
board = esp32-s3-devkitc-1 ;; generic dev board
custom_usermods = audioreactive
build_unflags = ${esp32s3.build_unflags} -DBOARD_HAS_PSRAM ;; make sure PSRAM support is removed
build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_8MB_none\"
-D WLED_WATCHDOG_TIMEOUT=0
;-D ARDUINO_USB_CDC_ON_BOOT=0 ;; -D ARDUINO_USB_MODE=1 ;; for boards with serial-to-USB chip
-D ARDUINO_USB_CDC_ON_BOOT=1 ;; -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB")
board_build.f_flash = 80000000L
board_build.flash_mode = qio
[env:esp32S3_wroom2]
;; For ESP32-S3 WROOM-2, a.k.a. ESP32-S3 DevKitC-1 v1.1
;; with >= 16MB FLASH and >= 8MB PSRAM (memory_type: opi_opi)
@@ -656,6 +729,21 @@ board_build.f_flash = 80000000L
board_build.flash_mode = qio
monitor_filters = esp32_exception_decoder
[env:esp32s3_4M_none]
;; ESP32-S3 with 4MB FLASH, no PSRAM
extends = esp32s3
board = esp32-s3-devkitc-1
custom_usermods = audioreactive
build_unflags = ${esp32s3.build_unflags} -D BOARD_HAS_PSRAM ;; make sure PSRAM support is removed
build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_4M_none\"
-DARDUINO_USB_CDC_ON_BOOT=1 ;; -DARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB")
-DLOLIN_WIFI_FIX ; seems to work much better with this
-D WLED_WATCHDOG_TIMEOUT=0
board_build.partitions = ${esp32.default_partitions}
board_build.f_flash = 80000000L
board_build.flash_mode = qio
[env:lolin_s2_mini]
platform = ${esp32s2.platform}
platform_packages = ${esp32s2.platform_packages}
@@ -694,3 +782,90 @@ monitor_filters = esp32_exception_decoder
board_build.flash_mode = dio
custom_usermods = * ; Expands to all usermods in usermods folder
board_build.partitions = ${esp32.extreme_partitions} ; We're gonna need a bigger boat
# ------------------------------------------------------------------------------
# Hub75 examples
# ------------------------------------------------------------------------------
# Note: some panels may experience ghosting with default full brightness. use -D WLED_HUB75_MAX_BRIGHTNESS=239 or lower to fix it.
[hub75]
;; Shared values for all HUB75 build envs.
;; Core HUB75 flags - common to every HUB75 build
build_flags =
-D WLED_ENABLE_HUB75MATRIX -D NO_GFX
-D 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: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}
+65 -137
View File
@@ -5,7 +5,7 @@
# Please visit documentation: https://docs.platformio.org/page/projectconf.html
[platformio]
default_envs = WLED_generic8266_1M, esp32dev_V4_dio80 # put the name(s) of your own build environment here. You can define as many as you need
default_envs = WLED_generic8266_1M, esp32dev_dio80 # put the name(s) of your own build environment here. You can define as many as you need
#----------
# SAMPLE
@@ -30,7 +30,10 @@ lib_deps = ${esp8266.lib_deps}
; https://github.com/blazoncek/QuickESPNow.git#optional-debug ;; exludes debug library
build_unflags = ${common.build_unflags}
build_flags = ${common.build_flags} ${esp8266.build_flags}
build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP8266_generic_1M\"
-D WLED_DISABLE_PARTICLESYSTEM1D -D WLED_DISABLE_PARTICLESYSTEM2D
-D WLED_DISABLE_PIXELFORGE
-D WLED_DISABLE_OTA -D WLED_DISABLE_2D
;
; *** To use the below defines/overrides, copy and paste each onto its own line just below build_flags in the section above.
;
@@ -47,7 +50,7 @@ build_flags = ${common.build_flags} ${esp8266.build_flags}
; -D WLED_DISABLE_MQTT
; -D WLED_DISABLE_ADALIGHT
; -D WLED_DISABLE_2D
; -D WLED_DISABLE_PXMAGIC
; -D WLED_DISABLE_PIXELFORGE
; -D WLED_DISABLE_ESPNOW
; -D WLED_DISABLE_BROWNOUT_DET
;
@@ -184,6 +187,7 @@ build_flags = ${common.build_flags} ${esp8266.build_flags}
; configure I2C and SPI interface (for various hardware)
; -D I2CSDAPIN=33 # initialise interface
; -D I2CSCLPIN=35 # initialise interface
; # HW_PIN_* informs the WebUI about default pins - don't initialise interface
; -D HW_PIN_SCL=35
; -D HW_PIN_SDA=33
; -D HW_PIN_CLOCKSPI=7
@@ -193,7 +197,8 @@ build_flags = ${common.build_flags} ${esp8266.build_flags}
# ------------------------------------------------------------------------------
# Optional: build flags for speed, instead of optimising for size.
# Example of usage: see [env:esp32S3_PSRAM_HUB75]
# Add ${Speed_Flags.build_flags} / ${Speed_Flags.build_unflags} to your own env
# in platformio_override.ini to opt in.
# ------------------------------------------------------------------------------
[Speed_Flags]
@@ -218,7 +223,8 @@ 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}
build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP8266_ESP07\" #-DWLED_DISABLE_2D
-D WLED_DISABLE_PARTICLESYSTEM2D
lib_deps = ${esp8266.lib_deps}
[env:d1_mini]
@@ -228,7 +234,8 @@ 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}
build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP8266_D1MINI\" #-DWLED_DISABLE_2D
-D WLED_DISABLE_PARTICLESYSTEM2D
lib_deps = ${esp8266.lib_deps}
monitor_filters = esp8266_exception_decoder
@@ -238,7 +245,8 @@ 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}
build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP8266_HT_D1MINI\" #-DWLED_DISABLE_2D
-D WLED_DISABLE_PARTICLESYSTEM2D
lib_deps = ${esp8266.lib_deps}
[env:h803wf]
@@ -247,30 +255,24 @@ 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
build_flags = ${common.build_flags} ${esp8266.build_flags} -D DATA_PINS=1 -D WLED_DISABLE_INFRARED -D WLED_RELEASE_NAME=\"ESP8266_HT803WF\" #-DWLED_DISABLE_2D
-D WLED_DISABLE_PARTICLESYSTEM2D
lib_deps = ${esp8266.lib_deps}
[env:esp32dev_qio80]
extends = env:esp32dev # we want to extend the existing esp32dev environment (and define only updated options)
board = esp32dev
build_flags = ${common.build_flags} ${esp32.build_flags} #-D WLED_DISABLE_BROWNOUT_DET
lib_deps = ${esp32.lib_deps}
build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=\"ESP32_qio80\" #-D WLED_DISABLE_BROWNOUT_DET
lib_deps = ${esp32_idf_V4.lib_deps}
monitor_filters = esp32_exception_decoder
board_build.f_flash = 80000000L
board_build.flash_mode = qio
[env:esp32dev_V4_dio80]
;; experimental ESP32 env using ESP-IDF V4.4.x
;; Warning: this build environment is not stable!!
;; please erase your device before installing.
extends = esp32_idf_V4 # based on newer "esp-idf V4" platform environment
board = esp32dev
build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} #-D WLED_DISABLE_BROWNOUT_DET
lib_deps = ${esp32_idf_V4.lib_deps}
monitor_filters = esp32_exception_decoder
board_build.partitions = ${esp32.default_partitions} ;; if you get errors about "out of program space", change this to ${esp32.extended_partitions} or even ${esp32.big_partitions}
board_build.f_flash = 80000000L
board_build.flash_mode = dio
[env:esp32dev_dio80]
extends = env:esp32dev_qio80 # we want to extend the previous environment, to change flash speed
build_unflags = ${env:esp32dev_qio80.build_unflags} -D WLED_RELEASE_NAME=\"ESP32_qio80\" # need to remove the previous WLED_RELEASE_NAME
build_flags = ${env:esp32dev_qio80.build_flags} -D WLED_RELEASE_NAME=\"ESP32_dio80\" # ... and then we can set a new one
board_build.flash_mode = dio # change flash mode to "dio", for boards that cannot not start with "qio" mode
[env:esp32s2_saola]
extends = esp32s2
@@ -280,25 +282,19 @@ platform_packages = ${esp32s2.platform_packages}
framework = arduino
board_build.flash_mode = qio
upload_speed = 460800
build_flags = ${common.build_flags} ${esp32s2.build_flags}
build_flags = ${common.build_flags} ${esp32s2.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S2_saola\"
;-DLOLIN_WIFI_FIX ;; try this in case Wifi does not work
-DARDUINO_USB_CDC_ON_BOOT=1
lib_deps = ${esp32s2.lib_deps}
[env:esp32s3dev_8MB_PSRAM_qspi]
;; ESP32-TinyS3 development board, with 8MB FLASH and PSRAM (memory_type: qio_qspi)
extends = env:esp32s3dev_8MB_PSRAM_opi
;board = um_tinys3 ; -> needs workaround from https://github.com/wled-dev/WLED/pull/2905#issuecomment-1328049860
board = esp32-s3-devkitc-1 ;; generic dev board; the next line adds PSRAM support
board_build.arduino.memory_type = qio_qspi ;; use with PSRAM: 2MB or 4MB
[env:esp8285_4CH_MagicHome]
board = esp8285
platform = ${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
build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_DISABLE_OTA -D WLED_RELEASE_NAME=\"ESP8285_4CH_MagicHome\" #-DWLED_DISABLE_2D
-D WLED_DISABLE_PARTICLESYSTEM2D
lib_deps = ${esp8266.lib_deps}
[env:esp8285_H801]
@@ -307,7 +303,8 @@ 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
build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_DISABLE_OTA -D WLED_RELEASE_NAME=\"ESP8285_H801\" #-DWLED_DISABLE_2D
-D WLED_DISABLE_PARTICLESYSTEM2D
lib_deps = ${esp8266.lib_deps}
[env:d1_mini_5CH_Shojo_PCB]
@@ -317,6 +314,8 @@ 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]
@@ -326,7 +325,8 @@ 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}
build_flags = ${common.build_flags} ${esp8266.build_flags} ${common.debug_flags} -D WLED_RELEASE_NAME=\"ESP8266_D1MINI_DEBUG\" #-DWLED_DISABLE_2D
-D WLED_DISABLE_PARTICLESYSTEM2D
lib_deps = ${esp8266.lib_deps}
[env:d1_mini_ota]
@@ -338,7 +338,8 @@ 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}
build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP8266_D1MINI_OTA\" #-DWLED_DISABLE_2D
-D WLED_DISABLE_PARTICLESYSTEM2D
lib_deps = ${esp8266.lib_deps}
[env:anavi_miracle_controller]
@@ -348,6 +349,8 @@ 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]
@@ -358,6 +361,7 @@ platform = ${esp32c3.platform}
platform_packages = ${esp32c3.platform_packages}
board = esp32-c3-devkitm-1
build_flags = ${common.build_flags} ${esp32c3.build_flags}
-D WLED_RELEASE_NAME=\"ESP32-C3_2MB\"
-D WLED_WATCHDOG_TIMEOUT=0
-D WLED_DISABLE_OTA
; -DARDUINO_USB_CDC_ON_BOOT=1 ;; for virtual CDC USB
@@ -409,12 +413,14 @@ board_build.f_flash = 80000000L
[env:m5atom]
extends = env:esp32dev # we want to extend the existing esp32dev environment (and define only updated options)
build_flags = ${common.build_flags} ${esp32.build_flags} -D DATA_PINS=27 -D BTNPIN=39
-D WLED_RELEASE_NAME=\"ESP32_m5atom\"
[env:sp501e]
board = esp_wroom_02
platform = ${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]
@@ -422,6 +428,7 @@ board = esp_wroom_02
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
@@ -431,7 +438,8 @@ 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 LED_TYPES=TYPE_ANALOG_5CH -D WLED_DISABLE_INFRARED -D WLED_MAX_CCT_BLEND=0
-D WLED_RELEASE_NAME=\"ESP8285_Athom_RGBCW\" -D WLED_DISABLE_PARTICLESYSTEM2D
lib_deps = ${esp8266.lib_deps}
[env:Athom_15w_RGBCW] ;15w bulb
@@ -441,7 +449,8 @@ 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 LED_TYPES=TYPE_ANALOG_5CH -D WLED_DISABLE_INFRARED -D WLED_MAX_CCT_BLEND=0 -D WLED_USE_IC_CCT
-D WLED_RELEASE_NAME=\"ESP8285_Athom_15W_RGBCW\" -D WLED_DISABLE_PARTICLESYSTEM2D
lib_deps = ${esp8266.lib_deps}
[env:Athom_3Pin_Controller] ;small controller with only data
@@ -451,6 +460,7 @@ 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
@@ -460,6 +470,7 @@ 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
@@ -469,6 +480,7 @@ 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]
@@ -477,7 +489,8 @@ 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 USERMOD_MY9291
custom_usermods = ${env:esp01_1m_full.custom_usermods} MY9291
build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_DISABLE_OTA -D WLED_RELEASE_NAME=\"ESP8266_MY9291\" -D WLED_DISABLE_PARTICLESYSTEM2D
lib_deps = ${esp8266.lib_deps}
# ------------------------------------------------------------------------------
@@ -491,7 +504,7 @@ 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}
build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP8266_CODM06_2MB\" -D WLED_DISABLE_PARTICLESYSTEM2D
lib_deps = ${esp8266.lib_deps}
[env:codm-controller-0_6-rev2]
@@ -500,7 +513,7 @@ 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}
build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP8266_CODM06R2_4MB\" -D WLED_DISABLE_PARTICLESYSTEM2D
lib_deps = ${esp8266.lib_deps}
# ------------------------------------------------------------------------------
@@ -538,105 +551,20 @@ monitor_filters = esp32_exception_decoder
# 433MHz RF remote example for esp32dev
[env:esp32dev_usermod_RF433]
extends = env:esp32dev
build_flags = ${env:esp32dev.build_flags} -D USERMOD_RF433
lib_deps = ${env:esp32dev.lib_deps}
sui77/rc-switch @ 2.6.4
custom_usermods =
${env:esp32dev.custom_usermods}
RF433
# ------------------------------------------------------------------------------
# Hub75 examples
# ------------------------------------------------------------------------------
[env:esp32dev_hub75]
board = esp32dev
upload_speed = 921600
platform = ${esp32_idf_V4.platform}
platform_packages =
build_unflags = ${common.build_unflags}
build_flags = ${common.build_flags}
-D WLED_RELEASE_NAME=\"ESP32_hub75\"
-D WLED_ENABLE_HUB75MATRIX -D NO_GFX
-D WLED_DEBUG_BUS
; -D WLED_DEBUG
-D SR_DMTYPE=1 -D I2S_SDPIN=-1 -D I2S_CKPIN=-1 -D I2S_WSPIN=-1 -D MCLK_PIN=-1 ;; Disable to prevent pin clash
lib_deps = ${esp32_idf_V4.lib_deps}
https://github.com/mrfaptastic/ESP32-HUB75-MatrixPanel-DMA.git#3.0.11
monitor_filters = esp32_exception_decoder
board_build.partitions = ${esp32.default_partitions}
board_build.flash_mode = dio
custom_usermods = audioreactive
[env:esp32dev_hub75_forum_pinout]
extends = env:esp32dev_hub75
build_flags = ${common.build_flags}
-D WLED_RELEASE_NAME=\"ESP32_hub75_forum_pinout\"
-D WLED_ENABLE_HUB75MATRIX -D NO_GFX
-D ESP32_FORUM_PINOUT ;; enable for SmartMatrix default pins
-D WLED_DEBUG_BUS
-D SR_DMTYPE=1 -D I2S_SDPIN=-1 -D I2S_CKPIN=-1 -D I2S_WSPIN=-1 -D MCLK_PIN=-1 ;; Disable to prevent pin clash
; -D WLED_DEBUG
# 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]
extends = env:esp32dev
custom_usermods =
${env:esp32dev.custom_usermods}
https://github.com/wled/wled-usermod-example.git#main
[env:adafruit_matrixportal_esp32s3]
; ESP32-S3 processor, 8 MB flash, 2 MB of PSRAM, dedicated driver pins for HUB75
board = adafruit_matrixportal_esp32s3
;; adafruit recommends to use arduino-esp32 2.0.14
;;platform = espressif32@ ~6.5.0
;;platform_packages = platformio/framework-arduinoespressif32 @ 3.20014.231204 ;; arduino-esp32 2.0.14
platform = ${esp32s3.platform}
platform_packages =
upload_speed = 921600
build_unflags = ${common.build_unflags}
build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_8M_qspi\"
-DARDUINO_USB_CDC_ON_BOOT=1 ;; -DARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB")
-DBOARD_HAS_PSRAM
-DLOLIN_WIFI_FIX ; seems to work much better with this
-D WLED_WATCHDOG_TIMEOUT=0
-D WLED_ENABLE_HUB75MATRIX -D NO_GFX
-D S3_LCD_DIV_NUM=20 ;; Attempt to fix wifi performance issue when panel active with S3 chips
-D ARDUINO_ADAFRUIT_MATRIXPORTAL_ESP32S3
-D WLED_DEBUG_BUS
-D SR_DMTYPE=1 -D I2S_SDPIN=-1 -D I2S_CKPIN=-1 -D I2S_WSPIN=-1 -D MCLK_PIN=-1 ;; Disable to prevent pin clash
lib_deps = ${esp32s3.lib_deps}
https://github.com/mrfaptastic/ESP32-HUB75-MatrixPanel-DMA.git#aa28e2a ;; S3_LCD_DIV_NUM fix
board_build.partitions = ${esp32.large_partitions} ;; standard bootloader and 8MB Flash partitions
;; board_build.partitions = tools/partitions-8MB_spiffs-tinyuf2.csv ;; supports adafruit UF2 bootloader
board_build.f_flash = 80000000L
board_build.flash_mode = qio
monitor_filters = esp32_exception_decoder
custom_usermods = audioreactive
[env:esp32S3_PSRAM_HUB75]
;; MOONHUB HUB75 adapter board (lilygo T7-S3 with 16MB flash and PSRAM)
board = lilygo-t7-s3
platform = ${esp32s3.platform}
platform_packages =
upload_speed = 921600
build_unflags = ${common.build_unflags}
${Speed_Flags.build_unflags} ;; optional: removes "-Os" so we can override with "-O2" in build_flags
build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"esp32S3_16MB_PSRAM_HUB75\"
${Speed_Flags.build_flags} ;; optional: -O2 -> optimize for speed instead of size
-DARDUINO_USB_CDC_ON_BOOT=1 ;; -DARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB")
-DBOARD_HAS_PSRAM
-DLOLIN_WIFI_FIX ; seems to work much better with this
-D WLED_WATCHDOG_TIMEOUT=0
-D WLED_ENABLE_HUB75MATRIX -D NO_GFX
-D S3_LCD_DIV_NUM=20 ;; Attempt to fix wifi performance issue when panel active with S3 chips
-D MOONHUB_S3_PINOUT ;; HUB75 pinout
-D WLED_DEBUG_BUS
-D LEDPIN=14 -D BTNPIN=0 -D RLYPIN=15 -D IRPIN=-1 -D AUDIOPIN=-1 ;; defaults that avoid pin conflicts with HUB75
-D SR_DMTYPE=1 -D I2S_SDPIN=10 -D I2S_CKPIN=11 -D I2S_WSPIN=12 -D MCLK_PIN=-1 ;; I2S mic
lib_deps = ${esp32s3.lib_deps}
https://github.com/mrfaptastic/ESP32-HUB75-MatrixPanel-DMA.git#aa28e2a ;; S3_LCD_DIV_NUM fix
;;board_build.partitions = ${esp32.large_partitions} ;; for 8MB flash
board_build.partitions = ${esp32.extreme_partitions} ;; for 16MB flash
board_build.f_flash = 80000000L
board_build.flash_mode = qio
monitor_filters = esp32_exception_decoder
custom_usermods = audioreactive
+63 -47
View File
@@ -7,81 +7,97 @@
<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 implementation of an ESP32 and ESP8266 webserver to control NeoPixel (WS2812B, WS2811, SK6812) LEDs or also SPI based chipsets like the WS2801 and APA102!
A fast and feature-rich firmware for ESP32 microcontrollers to control addressable LEDs — from simple strips to large 2D matrices and HUB75 panels.
Originally created by [Aircoookie](https://github.com/Aircoookie)
Originally created by [Aircoookie](https://github.com/Aircoookie), now maintained by a community of contributors.
## ⚙️ Features
- WS2812FX library with more than 100 special effects
- FastLED noise effects and 50 palettes
- Modern UI with color, effect and segment controls
- Segments to set different effects and colors to user defined parts of the LED string
- Settings page - configuration via the network
- Access Point and station mode - automatic failsafe AP
- [Up to 10 LED outputs](https://kno.wled.ge/features/multi-strip/#esp32) per instance
- Support for RGBW strips
- Up to 250 user presets to save and load colors/effects easily, supports cycling through them.
- Presets can be used to automatically execute API calls
- Nightlight function (gradually dims down)
- Full OTA software updateability (HTTP + ArduinoOTA), password protectable
- Configurable analog clock (Cronixie, 7-segment and EleksTube IPS clock support via usermods)
- Configurable Auto Brightness limit for safe operation
- Filesystem-based config for easier backup of presets and settings
## 💡 Supported light control interfaces
- WLED app for [Android](https://play.google.com/store/apps/details?id=ca.cgagnier.wlednativeandroid) and [iOS](https://apps.apple.com/gb/app/wled-native/id6446207239)
- JSON and HTTP request APIs
- MQTT
- E1.31, Art-Net, DDP and TPM2.net
- [diyHue](https://github.com/diyhue/diyHue) (Wled is supported by diyHue, including Hue Sync Entertainment under udp. Thanks to [Gregory Mallios](https://github.com/gmallios))
- [Hyperion](https://github.com/hyperion-project/hyperion.ng)
- UDP realtime
- Alexa voice control (including dimming and color)
- Sync to Philips hue lights
- Adalight (PC ambilight via serial) and TPM2
- Sync color of multiple WLED devices (UDP notifier)
- Infrared remotes (24-key RGB, receiver required)
- Simple timers/schedules (time from NTP, timezones/DST supported)
### Effects & Visuals
- [**200+ built-in effects**](https://kno.wled.ge/features/effects/) including classic animations, audio-reactive, and 2D/matrix effects
- [50+ color palettes](https://kno.wled.ge/features/palettes/) plus a built-in **custom palette editor** (PixelForge)
- [**2D LED matrix support**](https://kno.wled.ge/advanced/mapping/) with dedicated 2D effects and flexible panel mapping
- [**HUB75 RGB matrix panel support**](https://kno.wled.ge/advanced/HUB75/) (ESP32)
- [**AudioReactive**](https://kno.wled.ge/advanced/audio-reactive/) effects — included by default, responding to sound via microphone, line-in, or network audio source
- Effect blending for smooth transitions between animations
- Antialiased drawing functions for smooth graphics
### Segments & Control
- [**Segments**](https://kno.wled.ge/features/segments/) — apply different effects, colors and palettes to independent parts of your LED setup simultaneously
- Up to **250 presets** to save and recall colors, effects and segment configurations — supports [playlists](https://kno.wled.ge/features/presets/) for automated cycling
- Nightlight function with configurable dimming curve
- Configurable **Auto Brightness Limiter** (per output) for safe operation
### Hardware Support
- **ESP32** (all variants: original, S2, S3, C3)
- [**Up to 17 LED outputs**](https://kno.wled.ge/features/multi-strip/) on ESP32 using parallel I2S + RMT
- [Addressable LED support](https://kno.wled.ge/basics/compatible-led-strips/): WS2812B, WS2811, WS2815, SK6812, WS2805, TM1914, APA102, WS2801, LPD8806, and many more
- RGBW, [RGB+CCT](https://kno.wled.ge/features/cct/) and white-only strips
- PWM outputs for analog LEDs and dimmers
- [**Ethernet** support](https://kno.wled.ge/features/ethernet-lan/) for a wide range of boards (QuinLED, LILYGO, Olimex, and more)
- Filesystem-based config for easy backup and restore of presets and settings
- Full OTA firmware updates (HTTP + ArduinoOTA), password-protectable
### Connectivity & Integrations
- **WLED app** for [Android](https://play.google.com/store/apps/details?id=ca.cgagnier.wlednativeandroid) and [iOS](https://apps.apple.com/gb/app/wled-native/id6446207239)
- [JSON](https://kno.wled.ge/interfaces/json-api/) and [HTTP request](https://kno.wled.ge/interfaces/http-api/) APIs
- **Multi-WiFi** — connect to up to 3 networks with automatic AP fallback
- **ESP-NOW** wireless sync between devices (no WiFi router required)
- [**MQTT**](https://kno.wled.ge/interfaces/mqtt/) with Home Assistant discovery
- [**E1.31, Art-Net**](https://kno.wled.ge/interfaces/e1.31-dmx/), [DDP](https://kno.wled.ge/interfaces/ddp/) and [TPM2.net](https://kno.wled.ge/interfaces/udp-realtime/) for DMX/professional lighting control
- [UDP realtime sync](https://kno.wled.ge/interfaces/udp-notifier/) across multiple WLED devices
- Alexa voice control (on/off, brightness, color)
- [Philips Hue sync](https://kno.wled.ge/interfaces/philips-hue/)
- [diyHue](https://github.com/diyhue/diyHue) and [Hyperion](https://github.com/hyperion-project/hyperion.ng) integration
- [Adalight / TPM2](https://kno.wled.ge/interfaces/serial/) (PC ambilight via serial)
- [Infrared remote control](https://kno.wled.ge/interfaces/infrared/) (24-key RGB, receiver required)
- Timers and schedules (NTP time sync, full timezone and DST support)
### Developer-Friendly
- **Usermod system** — extend WLED with community or custom modules without modifying core code
- Large and active [usermod library](https://kno.wled.ge/advanced/community-usermods/) including AudioReactive, temperature sensors, rotary encoders, displays, and much more
- Well-documented [JSON API](https://kno.wled.ge/interfaces/json-api/)
- Licensed under the **EUPL v1.2**
## 📲 Quick start guide and documentation
See the [documentation on our official site](https://kno.wled.ge)!
See the [documentation at kno.wled.ge](https://kno.wled.ge)!
[On this page](https://kno.wled.ge/basics/tutorials/) you can find excellent tutorials and tools to help you get your new project up and running!
[Tutorials and getting-started guides](https://kno.wled.ge/basics/tutorials/) to help you get your project running quickly.
## 🖼️ User interface
<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 [here](https://kno.wled.ge/basics/compatible-hardware)!
See the [compatible hardware list](https://kno.wled.ge/basics/compatible-hardware) on the wiki.
## ✌️ Other
Licensed under the EUPL v1.2 license
Credits [here](https://kno.wled.ge/about/contributors/)!
CORS proxy by [Corsfix](https://corsfix.com/)
Licensed under the [EUPL v1.2](https://raw.githubusercontent.com/wled-dev/WLED/main/LICENSE).
Credits to all [contributors](https://kno.wled.ge/about/contributors/)!
CORS proxy by [Corsfix](https://corsfix.com/).
Join the Discord server to discuss everything about WLED!
<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)!
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 you'd like to reach the original creator privately: [dev.aircoookie@gmail.com](mailto:dev.aircoookie@gmail.com).
If WLED really brightens up your day, you can [![](https://img.shields.io/badge/send%20me%20a%20small%20gift-paypal-blue.svg?style=flat-square)](https://paypal.me/aircoookie)
If WLED brightens up your day, you can [send a gift to Aircoookie via PayPal](https://paypal.me/aircoookie).
---
*Disclaimer:*
*Disclaimer:*
If you are prone to photosensitive epilepsy, we recommended you do **not** use this software.
If you still want to try, don't use strobe, lighting or noise modes or high effect speed settings.
As per the EUPL license, I assume no liability for any damage to you or any other person or equipment.
If you are prone to photosensitive epilepsy, we recommend you do **not** use this software.
If you still want to try, avoid strobe, lightning or noise modes and high effect speed settings.
As per the EUPL license, no liability is assumed for any damage to you or any other person or equipment.
+46 -4
View File
@@ -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_settings.h", "wled00/html_other.h"]
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"]
// \x1b[34m is blue, \x1b[36m is cyan, \x1b[0m is reset
const wledBanner = `
@@ -38,6 +38,11 @@ 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.
@@ -45,6 +50,9 @@ 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()}
`;
@@ -126,12 +134,13 @@ async function minify(str, type = "plain") {
throw new Error("Unknown filter: " + type);
}
async function writeHtmlGzipped(sourceFile, resultFile, page) {
async function writeHtmlGzipped(sourceFile, resultFile, page, inlineCss = true) {
console.info("Reading " + sourceFile);
inline.html({
fileContent: fs.readFileSync(sourceFile, "utf8"),
relativeTo: path.dirname(sourceFile),
strict: true,
strict: inlineCss, // when not inlining css, ignore errors (enables linking style.css from subfolder htm files)
stylesheets: inlineCss // when true (default), css is inlined
},
async function (error, html) {
if (error) throw error;
@@ -244,10 +253,37 @@ 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",
@@ -358,6 +394,12 @@ 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"
@@ -0,0 +1,696 @@
<!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>
+1 -2
View File
@@ -3,7 +3,6 @@
/*
* Usermod for analog clock
*/
extern Timezone* tz;
class AnalogClockUsermod : public Usermod {
private:
@@ -116,7 +115,7 @@ private:
);
}
static inline uint32_t scale32(uint32_t c, fract8 scale) {
static inline uint32_t scale32(uint32_t c, uint8_t scale) {
return RGBW32(
scale8(R(c), scale),
scale8(G(c), scale),
+7 -7
View File
@@ -114,7 +114,7 @@ class MyExampleUsermod : public Usermod {
void loop() override {
// if usermod is disabled or called during strip updating just exit
// NOTE: on very long strips strip.isUpdating() may always return true so update accordingly
if (!enabled || strip.isUpdating()) return;
if (!enabled || (strip.isUpdating() && (millis() - lastTime < 200))) return; // adjust "200" (in millisecond) to your needs - prevents starvation with very long strips
// do your magic here
if (millis() - lastTime > 1000) {
@@ -176,7 +176,7 @@ class MyExampleUsermod : public Usermod {
JsonObject usermod = root[FPSTR(_name)];
if (!usermod.isNull()) {
// expect JSON usermod data in usermod name object: {"ExampleUsermod:{"user0":10}"}
userVar0 = usermod["user0"] | userVar0; //if "user0" key exists in JSON, update, else keep old value
userVar0 = usermod["user0"] | userVar0; //if "user0" key exists in JSON, update, else keep old value (userVar0 is defined in wled.h)
}
// you can as well check WLED state JSON keys
//if (root["bri"] == 255) Serial.println(F("Don't burn down your garage!"));
@@ -313,11 +313,11 @@ class MyExampleUsermod : public Usermod {
yield();
// ignore certain button types as they may have other consequences
if (!enabled
|| 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) {
|| 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) {
return false;
}
@@ -1,4 +1,5 @@
{
"name": "Fix_unreachable_netservices_v2",
"build": { "libArchive": false },
"platforms": ["espressif8266"]
}
@@ -77,7 +77,7 @@ public:
}
if (m_updateConfig)
{
serializeConfig();
serializeConfigToFS();
m_updateConfig = false;
}
}
+123
View File
@@ -0,0 +1,123 @@
#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);
+25
View File
@@ -0,0 +1,25 @@
## 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
+4
View File
@@ -0,0 +1,4 @@
{
"name": "PS Comet",
"build": { "libArchive": false }
}
+24 -12
View File
@@ -1,6 +1,6 @@
#include "UsermodTemperature.h"
static uint16_t mode_temperature();
static void mode_temperature();
//Dallas sensor quick (& dirty) reading. Credit to - Author: Peter Scargill, August 17th, 2013
float UsermodTemperature::readDallas() {
@@ -20,17 +20,19 @@ float UsermodTemperature::readDallas() {
}
#endif
switch(sensorFound) {
case 0x10: // DS18S20 has 9-bit precision
case 0x10: // DS18S20 has 9-bit precision - 1-bit fraction part
result = (data[1] << 8) | data[0];
retVal = float(result) * 0.5f;
break;
case 0x22: // DS18B20
case 0x28: // DS1822
case 0x22: // DS1822
case 0x28: // DS18B20
case 0x3B: // DS1825
case 0x42: // DS28EA00
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);
// 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)
break;
}
}
@@ -69,8 +71,8 @@ bool UsermodTemperature::findSensor() {
if (oneWire->crc8(deviceAddress, 7) == deviceAddress[7]) {
switch (deviceAddress[0]) {
case 0x10: // DS18S20
case 0x22: // DS18B20
case 0x28: // DS1822
case 0x22: // DS1822
case 0x28: // DS18B20
case 0x3B: // DS1825
case 0x42: // DS28EA00
DEBUG_PRINTLN(F("Sensor found."));
@@ -277,6 +279,7 @@ 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."));
}
@@ -304,6 +307,7 @@ 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
@@ -324,7 +328,7 @@ bool UsermodTemperature::readFromConfig(JsonObject &root) {
}
}
// use "return !top["newestParameter"].isNull();" when updating Usermod with new features
return !top[FPSTR(_domoticzIDX)].isNull();
return !top[FPSTR(_resolution)].isNull();
}
void UsermodTemperature::appendConfigData() {
@@ -332,6 +336,14 @@ 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() {
@@ -351,18 +363,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 uint16_t mode_temperature() {
static void 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,6 +48,7 @@ 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[];
@@ -56,6 +57,7 @@ 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[];
+52 -9
View File
@@ -5,9 +5,12 @@
#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;
@@ -31,16 +34,27 @@ 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;
if (*tetris->grid.getPixel(index_x, index_y) == 0)
{
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) {
//BG color
color = SEGCOLOR(1);
}
//game over animation
else if(*tetris->grid.getPixel(index_x, index_y) == 254)
else if (gridPixel == 254)
{
//use fg
color = SEGCOLOR(0);
@@ -48,7 +62,7 @@ void drawGrid(TetrisAIGame* tetris, TetrisAI_data* tetrisai_data)
else
{
//spread the color over the whole palette
uint8_t colorIndex = *tetris->grid.getPixel(index_x, index_y) * 32;
uint8_t colorIndex = gridPixel * 32;
colorIndex += tetrisai_data->colorOffset;
color = ColorFromPalette(SEGPALETTE, colorIndex, 255, NOBLEND);
}
@@ -98,13 +112,13 @@ void drawGrid(TetrisAIGame* tetris, TetrisAI_data* tetrisai_data)
////////////////////////////
// 2D Tetris AI //
////////////////////////////
uint16_t mode_2DTetrisAI()
void mode_2DTetrisAI()
{
if (!strip.isMatrix || !SEGENV.allocateData(sizeof(tetrisai_data)))
{
// not a 2D set-up
SEGMENT.fill(SEGCOLOR(0));
return 350;
return;
}
TetrisAI_data* tetrisai_data = reinterpret_cast<TetrisAI_data*>(SEGENV.data);
@@ -170,6 +184,7 @@ uint16_t 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));
}
@@ -184,7 +199,21 @@ uint16_t mode_2DTetrisAI()
tetrisai_data->tetris.ai.bumpiness = -0.184483f + dui;
}
if (tetrisai_data->tetris.state == TetrisAIGame::ANIMATE_MOVE)
//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 (strip.now - tetrisai_data->lastTime > msDelayMove)
@@ -222,8 +251,6 @@ uint16_t 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";
@@ -231,6 +258,7 @@ class TetrisAIUsermod : public Usermod
{
private:
static const char _name[];
public:
void setup()
@@ -238,6 +266,20 @@ 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()
{
@@ -249,6 +291,7 @@ public:
}
};
const char TetrisAIUsermod::_name[] PROGMEM = "TetrisAI_v2";
static TetrisAIUsermod tetrisai_v2;
REGISTER_USERMOD(tetrisai_v2);
+44 -5
View File
@@ -13,7 +13,6 @@
#ifndef __GRIDBW_H__
#define __GRIDBW_H__
#include <iterator>
#include <vector>
#include "pieces.h"
@@ -26,11 +25,18 @@ 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)
pixels(height),
clearingRows(height)
{
if (width > 32)
{
@@ -85,9 +91,26 @@ 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; )
@@ -95,8 +118,13 @@ public:
//full line?
if (isLineFull(row))
{
offset++;
pixels[row] = 0x0;
if (clearedLinesReadyForRemoval) {
offset++;
pixels[row] = 0x0;
doneRemovingClearedLines = true;
} else {
clearingRows[row] = true;
}
continue;
}
@@ -106,11 +134,20 @@ public:
pixels[row] = 0x0;
}
}
if (doneRemovingClearedLines) {
clearingRows.assign(height, false);
clearedLinesReadyForRemoval = false;
}
}
bool isLineFull(uint8_t y)
{
return pixels[y] == (uint32_t)((1 << width) - 1);
return pixels[y] == (width >= 32 ? UINT32_MAX : (1U << width) - 1);
}
bool isLineReadyForRemoval(uint8_t y)
{
return clearedLinesReadyForRemoval && isLineFull(y);
}
void reset()
@@ -122,6 +159,8 @@ public:
pixels.clear();
pixels.resize(height);
clearingRows.assign(height, false);
clearedLinesReadyForRemoval = false;
}
};
+1 -1
View File
@@ -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.isLineFull(y))
if (gridBW.isLineReadyForRemoval(y))
{
offset++;
for (uint8_t x = 0; x < width; x++)
-1
View File
@@ -19,7 +19,6 @@
#include <bitset>
#include <cstddef>
#include <cassert>
#include <iostream>
#define numPieces 7
+6 -3
View File
@@ -2,13 +2,16 @@
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.
Version 1.0
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.
## Installation
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)).
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)).
If needed simply add to `platformio_override.ini` (or `platformio_override.ini`):
If needed simply add to `platformio_override.ini`:
```ini
board_build.partitions = tools/WLED_ESP32_4MB_256KB_FS.csv
+1 -1
View File
@@ -68,7 +68,7 @@ public:
}
//line full if all ones in mask :-)
if (grid.isLineFull(row))
if (grid.isLineReadyForRemoval(row))
{
rating->fullLines++;
}
+3 -9
View File
@@ -15,7 +15,6 @@
#include <stdint.h>
#include <vector>
#include <algorithm>
#include "tetrisbag.h"
@@ -87,17 +86,12 @@ public:
void queuePiece()
{
//move vector to left
std::rotate(piecesQueue.begin(), piecesQueue.begin() + 1, piecesQueue.end());
for (uint8_t i = 1; i < piecesQueue.size(); i++) {
piecesQueue[i - 1] = piecesQueue[i];
}
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();
+326 -103
View File
@@ -6,10 +6,6 @@
#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))
@@ -24,6 +20,26 @@
* ....
*/
#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
@@ -103,6 +119,46 @@ 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"
@@ -112,14 +168,14 @@ constexpr int BLOCK_SIZE = 128; // I2S buffer size (samples)
// globals
static uint8_t inputLevel = 128; // UI slider value
#ifndef SR_SQUELCH
uint8_t soundSquelch = 10; // squelch value for volume reactive routines (config value)
static uint8_t soundSquelch = 10; // squelch value for volume reactive routines (config value)
#else
uint8_t soundSquelch = SR_SQUELCH; // squelch value for volume reactive routines (config value)
static uint8_t soundSquelch = SR_SQUELCH; // squelch value for volume reactive routines (config value)
#endif
#ifndef SR_GAIN
uint8_t sampleGain = 60; // sample gain (config value)
static uint8_t sampleGain = 60; // sample gain (config value)
#else
uint8_t sampleGain = SR_GAIN; // sample gain (config value)
static 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
@@ -144,8 +200,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 bandpass filter 80Hz-16Khz to remove noise. Applies before FFT.
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.
////////////////////
// Begin FFT Code //
////////////////////
@@ -153,7 +209,7 @@ static bool useBandPassFilter = 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, float *sampleBuffer); // pre-filtering of raw samples (band-pass)
static void runMicFilter(uint16_t numSamples, FFTsampleType *sampleBuffer);
static void postProcessFFTResults(bool noiseGateOpen, int numberOfChannels); // post-processing and post-amp of GEQ channels
static TaskHandle_t FFT_Task = nullptr;
@@ -189,13 +245,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.
@@ -204,16 +260,20 @@ static float* vImag = nullptr; // imaginary parts
// 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) {
float result = 0.0f;
FFTmathType result = 0;
for (int i = from; i <= to; i++) {
result += vReal[i];
result += valFFT[i];
}
return result / float(to - from + 1);
#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
}
//
@@ -222,18 +282,65 @@ 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 (vReal == nullptr) vReal = (float*) calloc(samplesFFT, sizeof(float));
if (vImag == nullptr) vImag = (float*) calloc(samplesFFT, sizeof(float));
if ((vReal == nullptr) || (vImag == nullptr)) {
if (valFFT == nullptr) valFFT = (float*) calloc(samplesFFT, sizeof(float));
if (vImag == nullptr) vImag = (float*) calloc(samplesFFT, sizeof(float));
if ((valFFT == nullptr) || (vImag == nullptr)) {
// something went wrong
if (vReal) free(vReal); vReal = nullptr;
if (valFFT) free(valFFT); valFFT = nullptr;
if (vImag) free(vImag); vImag = nullptr;
return;
}
// Create FFT object with weighing factor storage
ArduinoFFT<float> FFT = ArduinoFFT<float>( vReal, vImag, samplesFFT, SAMPLE_RATE, true);
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
if (valFFT == nullptr) {
valFFT = (float*)heap_caps_aligned_calloc(16, 2 * samplesFFT, sizeof(float), MALLOC_CAP_8BIT); // SIMD requires aligned memory to 16-byte boundary. note in IDF5 there is MALLOC_CAP_SIMD available
if ((valFFT == nullptr)) return; // something went wrong
}
// create window
if (windowFFT == nullptr) {
windowFFT = (float*)heap_caps_aligned_calloc(16, samplesFFT, sizeof(float), MALLOC_CAP_8BIT); // SIMD requires aligned memory to 16-byte boundary. note in IDF5 there is MALLOC_CAP_SIMD available
if ((windowFFT == nullptr)) {
heap_caps_free(valFFT); valFFT = nullptr;
return; // something went wrong
}
}
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
// use integer FFT - allocate and initialize integer FFT buffers on first call, 4 bytes aligned (just in case, even if not strictly needed for int16_t)
if (valFFT == nullptr) valFFT = (int16_t*) heap_caps_aligned_calloc(4, samplesFFT * 2, sizeof(int16_t), MALLOC_CAP_8BIT);
// create window
if (windowFFT == nullptr) windowFFT = (int16_t*) heap_caps_aligned_calloc(4, samplesFFT, sizeof(int16_t), MALLOC_CAP_8BIT);
// create window function for FFT
float *windowFloat = (float*) heap_caps_aligned_calloc(4, samplesFFT, sizeof(float), MALLOC_CAP_8BIT); // temporary buffer for window function
if (windowFloat == nullptr || windowFFT == nullptr || valFFT == nullptr) { // something went wrong
if (windowFloat) heap_caps_free(windowFloat);
if (windowFFT) heap_caps_free(windowFFT); windowFFT = nullptr;
if (valFFT) heap_caps_free(valFFT); valFFT = nullptr;
return;
}
if (dsps_fft2r_init_sc16(NULL, samplesFFT) != ESP_OK) return; // initialize FFT tables
#ifdef FFT_PREFER_EXACT_PEAKS
dsps_wind_blackman_harris_f32(windowFloat, samplesFFT);
#else
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);
}
heap_caps_free(windowFloat); // free temporary buffer
#endif
// see https://www.freertos.org/vtaskdelayuntil.html
const TickType_t xFrequency = FFT_MIN_CYCLE * portTICK_PERIOD_MS;
@@ -255,8 +362,7 @@ void FFTcode(void * parameter)
#endif
// get a fresh batch of samples from I2S
if (audioSource) audioSource->getSamples(vReal, samplesFFT);
memset(vImag, 0, samplesFFT * sizeof(float)); // set imaginary parts to 0
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 defined(WLED_DEBUG) || defined(SR_DEBUG)
if (start < esp_timer_get_time()) { // filter out overflows
@@ -268,16 +374,15 @@ void FFTcode(void * parameter)
xLastWakeTime = xTaskGetTickCount(); // update "last unblocked time" for vTaskDelay
// band pass filter - can reduce noise floor by a factor of 50
// band pass filter - can reduce noise floor by a factor of 50 and avoid aliasing effects to base & high frequency bands
// downside: frequencies below 100Hz will be ignored
if (useBandPassFilter) runMicFilter(samplesFFT, vReal);
if (useMicFilter) runMicFilter(samplesFFT, valFFT);
// find highest sample in the batch
float maxSample = 0.0f; // max sample from FFT batch
FFTsampleType maxSample = 0; // 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 ((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]);
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]);
}
// 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.
@@ -289,32 +394,96 @@ 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
// run FFT (takes 3-5ms on ESP32, ~12ms on ESP32-S2)
#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
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
//FFT.windowing(FFTWindow::Blackman_Harris, FFTDirection::Forward); // Weigh data using "Blackman- Harris" window - sharp peaks due to excellent sideband rejection
#endif
FFT.compute( FFTDirection::Forward ); // Compute FFT
FFT.complexToMagnitude(); // Compute magnitudes
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
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;
#endif
#endif
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.
memset(vReal, 0, samplesFFT * sizeof(float));
} 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));
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
@@ -345,7 +514,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);
@@ -407,12 +576,15 @@ void FFTcode(void * parameter)
// Pre / Postprocessing //
///////////////////////////
static void runMicFilter(uint16_t numSamples, float *sampleBuffer) // pre-filtering of raw samples (band-pass)
static void runMicFilter(uint16_t numSamples, FFTsampleType *sampleBuffer) // pre-filtering of raw samples (band-pass)
{
// low frequency cutoff parameter - see https://dsp.stackexchange.com/questions/40462/exponential-moving-average-cut-off-frequency
#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)
//constexpr float alpha = 0.04f; // 150Hz
//constexpr float alpha = 0.03f; // 110Hz
constexpr float alpha = 0.0225f; // 80hz
//constexpr float alpha = 0.0285f; //100Hz
constexpr float alpha = 0.0256f; //90Hz
//constexpr float alpha = 0.0225f; // 80hz
//constexpr float alpha = 0.01693f;// 60hz
// high frequency cutoff parameter
//constexpr float beta1 = 0.75f; // 11Khz
@@ -436,6 +608,39 @@ static void runMicFilter(uint16_t numSamples, float *sampleBuffer) // p
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
@@ -524,7 +729,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) && (vReal[binNum] > maxVol) && ((millis() - timeOfPeak) > 100)) {
if ((sampleAvg > 1) && (maxVol > 0) && (binNum > 4) && (valFFT[binNum] > maxVol) && ((millis() - timeOfPeak) > 100)) {
havePeak = true;
}
@@ -556,6 +761,8 @@ 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
@@ -677,6 +884,9 @@ 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[];
@@ -1093,7 +1303,12 @@ class AudioReactive : public Usermod {
size_t packetSize = fftUdp.parsePacket();
#ifdef ARDUINO_ARCH_ESP32
if ((packetSize > 0) && ((packetSize < 5) || (packetSize > UDPSOUND_MAX_PACKET))) fftUdp.flush(); // discard invalid packets (too small or too big) - only works on esp32
if ((packetSize > 0) && ((packetSize < 5) || (packetSize > UDPSOUND_MAX_PACKET)))
#if ESP_IDF_VERSION_MAJOR < 5
fftUdp.flush(); // discard invalid packets (too small or too big) - only works on esp32
#else
fftUdp.clear(); // function was renamed in newer frameworks
#endif
#endif
if ((packetSize > 5) && (packetSize <= UDPSOUND_MAX_PACKET)) {
//DEBUGSR_PRINTLN("Received UDP Sync Packet");
@@ -1169,8 +1384,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;
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)
#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
@@ -1205,12 +1420,13 @@ 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: I2S PDM Microphone - ")); DEBUGSR_PRINTLN(F(I2S_PDM_MIC_CHANNEL_TEXT));
DEBUGSR_PRINT(F("AR: Generic 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);
@@ -1220,6 +1436,7 @@ 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;
@@ -1235,7 +1452,7 @@ class AudioReactive : public Usermod {
break;
#endif
case 254: // dummy "network receive only" mode
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
@@ -1252,19 +1469,25 @@ class AudioReactive : public Usermod {
}
delay(250); // give microphone enough time to initialise
if (!audioSource && (dmType != 254)) enabled = false;// audio failed to initialise
if (!audioSource && (dmType != SR_DMTYPE_NETWORK_ONLY)) enabled = false;// audio failed to initialise
#endif
if (enabled) onUpdateBegin(false); // create FFT task, and initialize network
#ifdef ARDUINO_ARCH_ESP32
if (FFT_Task == nullptr) enabled = false; // FFT task creation failed
if (audioSource && 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
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
#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
disableSoundProcessing = true;
}
#endif
@@ -1318,14 +1541,9 @@ class AudioReactive : public Usermod {
// We cannot wait indefinitely before processing audio data
if (strip.isUpdating() && (millis() - lastUMRun < 2)) return; // be nice, but not too nice
// suspend local sound processing when "real time mode" is active (E131, UDP, ADALIGHT, ARTNET)
if ( (realtimeOverride == REALTIME_OVERRIDE_NONE) // please add other overrides here if needed
&&( (realtimeMode == REALTIME_MODE_GENERIC)
||(realtimeMode == REALTIME_MODE_E131)
||(realtimeMode == REALTIME_MODE_UDP)
||(realtimeMode == REALTIME_MODE_ADALIGHT)
||(realtimeMode == REALTIME_MODE_ARTNET) ) ) // please add other modes here if needed
{
// suspend local sound processing when "real time mode" is active (E131, UDP, ADALIGHT, ARTNET, DDP, DMX)
// exception: sound input is still needed when useMainSegmentOnly - other segments are still running with local input.
if (realtimeMode && !realtimeOverride && !useMainSegmentOnly) {
#if defined(ARDUINO_ARCH_ESP32) && defined(WLED_DEBUG)
if ((disableSoundProcessing == false) && (audioSyncEnabled == 0)) { // we just switched to "disabled"
DEBUG_PRINTLN(F("[AR userLoop] realtime mode active - audio processing suspended."));
@@ -1335,7 +1553,7 @@ class AudioReactive : public Usermod {
disableSoundProcessing = true;
} else {
#if defined(ARDUINO_ARCH_ESP32) && defined(WLED_DEBUG)
if ((disableSoundProcessing == true) && (audioSyncEnabled == 0) && audioSource->isInitialized()) { // we just switched to "enabled"
if ((disableSoundProcessing == true) && (audioSyncEnabled == 0) && audioSource && 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));
}
@@ -1347,7 +1565,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->isInitialized()) disableSoundProcessing = true; // no audio source
if (!audioSource || !audioSource->isInitialized()) disableSoundProcessing = true; // no audio source
// Only run the sampling code IF we're not in Receive mode or realtime mode
@@ -1402,7 +1620,11 @@ class AudioReactive : public Usermod {
have_new_sample = receiveAudioData();
if (have_new_sample) last_UDPTime = millis();
#ifdef ARDUINO_ARCH_ESP32
#if ESP_IDF_VERSION_MAJOR < 5
else fftUdp.flush(); // Flush udp input buffers if we haven't read it - avoids hickups in receive mode. Does not work on 8266.
#else
else fftUdp.clear(); // function was renamed in newer frameworks
#endif
#endif
lastTime = millis();
}
@@ -1507,7 +1729,7 @@ class AudioReactive : public Usermod {
);
}
micDataReal = 0.0f; // just to be sure
if (enabled) disableSoundProcessing = false;
if (enabled) disableSoundProcessing = false; // allows FFT_Task to run at least once, even when loop() might disable again
updateIsRunning = init;
}
@@ -1544,7 +1766,7 @@ class AudioReactive : public Usermod {
// better would be for AudioSource to implement getType()
if (enabled
&& dmType == 0 && audioPin>=0
&& (buttonType[b] == BTN_TYPE_ANALOG || buttonType[b] == BTN_TYPE_ANALOG_INVERTED)
&& (buttons[b].type == BTN_TYPE_ANALOG || buttons[b].type == BTN_TYPE_ANALOG_INVERTED)
) {
return true;
}
@@ -1628,7 +1850,8 @@ class AudioReactive : public Usermod {
if (audioSource->getType() == AudioSource::Type_I2SAdc) {
infoArr.add(F("ADC analog"));
} else {
infoArr.add(F("I2S digital"));
if (dmType == 5) infoArr.add(F("PDM digital")); // dmType 5 => generic PDM microphone
else infoArr.add(F("I2S digital"));
}
// input level or "silence"
if (maxSample5sec > 1.0f) {
@@ -1746,14 +1969,10 @@ 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 && customPalettes.size()<10) {
if (initDone && enabled && addPalettes && palettes==0) {
// if palettes were removed during JSON call re-add them
createAudioPalettes();
}
@@ -1915,9 +2134,12 @@ 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 I2S PDM',5);"));
uiScript.print(F("addOption(dd,'Generic PDM',5);"));
#endif
uiScript.print(F("addOption(dd,'ES8388',6);"));
uiScript.print(F("addOption(dd,'None - network receive only',"));
uiScript.print(SR_DMTYPE_NETWORK_ONLY);
uiScript.print(F(");"));
uiScript.print(F("dd=addDropdown(ux,'config:AGC');"));
uiScript.print(F("addOption(dd,'Off',0);"));
@@ -1982,24 +2204,21 @@ class AudioReactive : public Usermod {
void AudioReactive::removeAudioPalettes(void) {
DEBUG_PRINTLN(F("Removing audio palettes."));
while (palettes>0) {
customPalettes.pop_back();
DEBUG_PRINTLN(palettes);
palettes--;
}
DEBUG_PRINT(F("Total # of palettes: ")); DEBUG_PRINTLN(customPalettes.size());
palettes -= (int8_t)removeUsermodPalettes(_name);
if (palettes < 0) palettes = 0; // safeguard
}
void AudioReactive::createAudioPalettes(void) {
DEBUG_PRINT(F("Total # of palettes: ")); DEBUG_PRINTLN(customPalettes.size());
if (palettes) return;
DEBUG_PRINTLN(F("Adding audio palettes."));
for (int i=0; i<MAX_PALETTES; i++)
if (customPalettes.size() < WLED_MAX_CUSTOM_PALETTES) {
customPalettes.push_back(CRGBPalette16(CRGB(BLACK)));
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()
palettes++;
DEBUG_PRINTLN(palettes);
} else break;
}
}
// credit @netmindz ar palette, adapted for usermod @blazoncek
@@ -2011,12 +2230,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);
hsv2rgb_rainbow(hsv, value); // convert to R,G,B
value = hsv; // 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
hsv2rgb_rainbow(hsv, value); // convert to R,G,B
value = hsv; // convert to R,G,B
break;
default:
if (x == 1) {
@@ -2033,9 +2252,10 @@ CRGB AudioReactive::getCRGBForBand(int x, int pal) {
void AudioReactive::fillAudioPalettes() {
if (!palettes) return;
size_t lastCustPalette = customPalettes.size();
if (int(lastCustPalette) >= palettes) lastCustPalette -= palettes;
for (int pal=0; pal<palettes; pal++) {
// 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;
uint8_t tcp[16]; // Needs to be 4 times however many colors are being used.
// 3 colors = 12, 4 colors = 16, etc.
@@ -2043,26 +2263,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;
customPalettes[lastCustPalette+pal].loadDynamicGradientPalette(tcp);
ump.palette.loadDynamicGradientPalette(tcp);
}
}
@@ -2077,7 +2297,10 @@ 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::_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::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
+37 -18
View File
@@ -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_ESP32C3) || 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_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, 4, 6))
#if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0)) && (ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(4, 5, 0))
// 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(float *buffer, uint16_t num_samples) = 0;
virtual void getSamples(FFTsampleType *buffer, uint16_t num_samples) = 0;
/* check if the audio source driver was initialized successfully */
virtual bool isInitialized(void) {return(_initialized);}
@@ -225,15 +225,17 @@ 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 = true; // experimental - use aPLL clock source to improve sampling quality
_config.use_apll = false; // don't use aPLL clock source (fix for #5391)
#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)
@@ -314,7 +316,7 @@ class I2SSource : public AudioSource {
if (_mclkPin != I2S_PIN_NO_CHANGE) PinManager::deallocatePin(_mclkPin, PinOwner::UM_Audioreactive);
}
virtual void getSamples(float *buffer, uint16_t num_samples) {
virtual void getSamples(FFTsampleType *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 */
@@ -332,19 +334,36 @@ class I2SSource : public AudioSource {
return;
}
// 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)
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
currSample = (float) newSamples[i]; // 16bit input -> use as-is
// 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
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
buffer[i] *= _sampleScale; // scale samples
#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;
#endif
}
}
}
@@ -622,7 +641,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_11); // configure ADC input amplification
adc1_config_channel_atten(adc1_channel_t(channel), ADC_ATTEN_DB_12); // configure ADC input amplification
#if defined(I2S_GRAB_ADC1_COMPLETELY)
// according to docs from espressif, the ADC needs to be started explicitly
@@ -687,7 +706,7 @@ class I2SAdcSource : public I2SSource {
}
void getSamples(float *buffer, uint16_t num_samples) {
void getSamples(FFTsampleType *buffer, uint16_t num_samples) {
/* Enable ADC. This has to be enabled and disabled directly before and
* after sampling, otherwise Wifi dies
*/
+169 -50
View File
@@ -1,6 +1,8 @@
#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
@@ -21,27 +23,34 @@
#define DEEPSLEEP_DELAY 1
#endif
RTC_DATA_ATTR bool powerup = true; // variable in RTC data persists on a reboot
#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
class DeepSleepUsermod : public Usermod {
private:
bool enabled = true;
bool enabled = false; // do not enable by default
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 = 5; // delay deep sleep at bootup until preset settings are applied
int delaycounter = 10; // delay deep sleep at bootup until preset settings are applied, force wake up if offmode persists after bootup
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
#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
if (wakePin == 0 || wakePin == 2 || wakePin == 4 || (wakePin >= 12 && wakePin <= 15) || (wakePin >= 25 && wakePin <= 27) || (wakePin >= 32 && wakePin <= 39)) {
return true;
}
@@ -60,8 +69,57 @@ class DeepSleepUsermod : public Usermod {
return false;
}
public:
// 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
@@ -69,26 +127,40 @@ 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 || !offMode) { // disabled or LEDs are on
if (!enabled) return;
if (!offMode) { // 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) {
if(lastLoopTime == 0) lastLoopTime = millis(); // initialize
if (millis() - lastLoopTime < sleepDelay * 1000) {
return; // wait until delay is over
}
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(powerup == false && delaycounter) { // delay sleep in case a preset is being loaded and turnOnAtBoot is disabled (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 (beginStrip() / handleIO() does enable offMode temporarily in this case)
delaycounter--;
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
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
else bri = briS;
strip.setBrightness(bri); // needed to make handleIO() not turn off LEDs (really? does not help in bootup preset)
offMode = false;
@@ -99,57 +171,81 @@ 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)
if(wakeupAfter)
esp_sleep_enable_timer_wakeup((uint64_t)wakeupAfter * (uint64_t)1e6); //sleep for x seconds
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 defined(CONFIG_IDF_TARGET_ESP32C3) // ESP32 C3
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);
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);
else
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);
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
#else // ESP32, S2, S3
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_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
rtc_gpio_pulldown_dis((gpio_num_t)wakeupPin);
}
else { // enable pullup/pulldown resistor for RTC use
if(wakeWhenHigh)
rtc_gpio_pulldown_en((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
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
halerror = esp_sleep_enable_ext1_wakeup(1ULL << wakeupPin, ESP_EXT1_WAKEUP_ALL_LOW);
delay(1); // wait for pin to be ready
if(halerror == ESP_OK) esp_deep_sleep_start(); // go into deep sleep
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
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;
@@ -157,6 +253,11 @@ void addToConfig(JsonObject& root) override
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;
}
@@ -176,6 +277,11 @@ void addToConfig(JsonObject& root) override
}
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);
@@ -200,6 +306,19 @@ void addToConfig(JsonObject& root) override
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);"));
@@ -207,6 +326,7 @@ void addToConfig(JsonObject& root) override
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
}
@@ -217,7 +337,6 @@ void addToConfig(JsonObject& root) override
uint16_t getId() {
return USERMOD_ID_DEEP_SLEEP;
}
};
// add more strings here to reduce flash memory usage
+3 -2
View File
@@ -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 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.***
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.***
# 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
- 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 S3: GPIO 0-21
- ESP32 S2: GPIO 0-21
- ESP32 C3: GPIO 0-5
@@ -61,6 +61,7 @@ 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`
+28 -28
View File
@@ -562,11 +562,11 @@ void MultiRelay::loop() {
bool MultiRelay::handleButton(uint8_t b) {
yield();
if (!enabled
|| 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) {
|| 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) {
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 (buttonType[b] == BTN_TYPE_SWITCH) {
if (buttons[b].type == BTN_TYPE_SWITCH) {
//handleSwitch(b);
if (buttonPressedBefore[b] != isButtonPressed(b)) {
buttonPressedTime[b] = now;
buttonPressedBefore[b] = !buttonPressedBefore[b];
if (buttons[b].pressedBefore != isButtonPressed(b)) {
buttons[b].pressedTime = now;
buttons[b].pressedBefore = !buttons[b].pressedBefore;
}
if (buttonLongPressed[b] == buttonPressedBefore[b]) return handled;
if (buttons[b].longPressed == buttons[b].pressedBefore) return handled;
if (now - buttonPressedTime[b] > WLED_DEBOUNCE_THRESHOLD) { //fire edge event only after 50ms without change (debounce)
if (now - buttons[b].pressedTime > 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, buttonPressedBefore[b]);
buttonLongPressed[b] = buttonPressedBefore[b]; //save the last "long term" switch state
switchRelay(i, buttons[b].pressedBefore);
buttons[b].longPressed = buttons[b].pressedBefore; //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 (!buttonPressedBefore[b]) buttonPressedTime[b] = now;
buttonPressedBefore[b] = true;
if (!buttons[b].pressedBefore) buttons[b].pressedTime = now;
buttons[b].pressedBefore = true;
if (now - buttonPressedTime[b] > 600) { //long press
if (now - buttons[b].pressedTime > 600) { //long press
//longPressAction(b); //not exposed
//handled = false; //use if you want to pass to default behaviour
buttonLongPressed[b] = true;
buttons[b].longPressed = true;
}
} else if (!isButtonPressed(b) && buttonPressedBefore[b]) { //released
} else if (!isButtonPressed(b) && buttons[b].pressedBefore) { //released
long dur = now - buttonPressedTime[b];
long dur = now - buttons[b].pressedTime;
if (dur < WLED_DEBOUNCE_THRESHOLD) {
buttonPressedBefore[b] = false;
buttons[b].pressedBefore = false;
return handled;
} //too short "press", debounce
bool doublePress = buttonWaitTime[b]; //did we have short press before?
buttonWaitTime[b] = 0;
bool doublePress = buttons[b].waitTime; //did we have short press before?
buttons[b].waitTime = 0;
if (!buttonLongPressed[b]) { //short press
if (!buttons[b].longPressed) { //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 {
buttonWaitTime[b] = now;
buttons[b].waitTime = now;
}
}
buttonPressedBefore[b] = false;
buttonLongPressed[b] = false;
buttons[b].pressedBefore = false;
buttons[b].longPressed = false;
}
// if 350ms elapsed since last press/release it is a short press
if (buttonWaitTime[b] && now - buttonWaitTime[b] > 350 && !buttonPressedBefore[b]) {
buttonWaitTime[b] = 0;
if (buttons[b].waitTime && now - buttons[b].waitTime > 350 && !buttons[b].pressedBefore) {
buttons[b].waitTime = 0;
//shortPressAction(b); //not exposed
for (int i=0; i<MULTI_RELAY_MAX_RELAYS; i++) {
if (_relay[i].button == b) {
@@ -0,0 +1,48 @@
# 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
+18 -17
View File
@@ -8,10 +8,10 @@
#include "dice_state.h"
// Reuse FX display functions.
extern uint16_t mode_breath();
extern uint16_t mode_blends();
extern uint16_t mode_glitter();
extern uint16_t mode_gravcenter();
extern void mode_breath();
extern void mode_blends();
extern void mode_glitter();
extern void 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 (strip.paletteBlend == 1 || strip.paletteBlend == 3)
static uint16_t running_copy(uint32_t color1, uint32_t color2, bool theatre = false) {
#define PALETTE_SOLID_WRAP (paletteBlend == 1 || paletteBlend == 3)
static void 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,10 +63,9 @@ static uint16_t running_copy(uint32_t color1, uint32_t color2, bool theatre = fa
SEGENV.aux0 = (SEGENV.aux0 +1) % (theatre ? width : (width<<1));
SEGENV.step = it;
}
return FRAMETIME;
}
static uint16_t simple_roll() {
static void simple_roll() {
auto roll = GetLastRollForSegment();
if (roll.state != pixels::RollState::ON_FACE) {
SEGMENT.fill(0);
@@ -79,7 +78,6 @@ static uint16_t simple_roll() {
SEGMENT.setPixelColor(i, SEGCOLOR(1));
}
}
return FRAMETIME;
}
// See https://kno.wled.ge/interfaces/json-api/#effect-metadata
// Name - DieSimple
@@ -92,31 +90,34 @@ static uint16_t simple_roll() {
static const char _data_FX_MODE_SIMPLE_DIE[] PROGMEM =
"DieSimple@,,Selected Die;!,!;;1;c1=255";
static uint16_t pulse_roll() {
static void pulse_roll() {
auto roll = GetLastRollForSegment();
if (roll.state != pixels::RollState::ON_FACE) {
return mode_breath();
mode_breath();
return;
} else {
uint16_t ret = mode_blends();
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 uint16_t check_roll() {
static void check_roll() {
auto roll = GetLastRollForSegment();
if (roll.state != pixels::RollState::ON_FACE) {
return running_copy(SEGCOLOR(0), SEGCOLOR(2));
running_copy(SEGCOLOR(0), SEGCOLOR(2));
return;
} else {
if (roll.current_face + 1 >= SEGMENT.custom2) {
return mode_glitter();
mode_glitter();
return;
} else {
return mode_gravcenter();
mode_gravcenter();
return;
}
}
}
-8
View File
@@ -1,8 +0,0 @@
{
"name": "pixels_dice_tray",
"build": { "libArchive": false},
"dependencies": {
"arduino-pixels-dice":"https://github.com/axlan/arduino-pixels-dice.git",
"BLE":"*"
}
}
+23 -23
View File
@@ -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
|| 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) {
|| 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) {
return false;
}
@@ -476,43 +476,43 @@ class PixelsDiceTrayUsermod : public Usermod {
static unsigned long buttonWaitTime[2] = {0};
//momentary button logic
if (!buttonLongPressed[b] && isButtonPressed(b)) { //pressed
if (!buttonPressedBefore[b]) {
buttonPressedTime[b] = now;
if (!buttons[b].longPressed && isButtonPressed(b)) { //pressed
if (!buttons[b].pressedBefore) {
buttons[b].pressedTime = now;
}
buttonPressedBefore[b] = true;
buttons[b].pressedBefore = true;
if (now - buttonPressedTime[b] > WLED_LONG_PRESS) { //long press
if (now - buttons[b].pressedTime > WLED_LONG_PRESS) { //long press
menu_ctrl.HandleButton(ButtonType::LONG, b);
buttonLongPressed[b] = true;
buttons[b].longPressed = true;
return true;
}
} else if (!isButtonPressed(b) && buttonPressedBefore[b]) { //released
} else if (!isButtonPressed(b) && buttons[b].pressedBefore) { //released
long dur = now - buttonPressedTime[b];
long dur = now - buttons[b].pressedTime;
if (dur < WLED_DEBOUNCE_THRESHOLD) {
buttonPressedBefore[b] = false;
buttons[b].pressedBefore = false;
return true;
} //too short "press", debounce
bool doublePress = buttonWaitTime[b]; //did we have short press before?
buttonWaitTime[b] = 0;
bool doublePress = buttons[b].waitTime; //did we have short press before?
buttons[b].waitTime = 0;
if (!buttonLongPressed[b]) { //short press
if (!buttons[b].longPressed) { //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 {
buttonWaitTime[b] = now;
buttons[b].waitTime = now;
}
}
buttonPressedBefore[b] = false;
buttonLongPressed[b] = false;
buttons[b].pressedBefore = false;
buttons[b].longPressed = false;
}
// if 350ms elapsed since last press/release it is a short press
if (buttonWaitTime[b] && now - buttonWaitTime[b] > WLED_DOUBLE_PRESS &&
!buttonPressedBefore[b]) {
buttonWaitTime[b] = 0;
if (buttons[b].waitTime && now - buttons[b].waitTime > WLED_DOUBLE_PRESS &&
!buttons[b].pressedBefore) {
buttons[b].waitTime = 0;
menu_ctrl.HandleButton(ButtonType::SINGLE, b);
}
+7 -7
View File
@@ -5,37 +5,37 @@ static const char _data_FX_MODE_POV_IMAGE[] PROGMEM = "POV Image@!;;;;";
static POV s_pov;
uint16_t mode_pov_image(void) {
void mode_pov_image(void) {
Segment& mainseg = strip.getMainSegment();
const char* segName = mainseg.name;
if (!segName) {
return FRAMETIME;
return;
}
// Only proceed for files ending with .bmp (case-insensitive)
size_t segLen = strlen(segName);
if (segLen < 4) return FRAMETIME;
if (segLen < 4) return;
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 FRAMETIME;
return;
}
const char* current = s_pov.getFilename();
if (current && strcmp(segName, current) == 0) {
s_pov.showNextLine();
return FRAMETIME;
return;
}
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 FRAMETIME;
if (nowMs - s_lastLoadAttemptMs < 500) return;
s_lastLoadAttemptMs = nowMs;
s_pov.loadImage(segName);
return FRAMETIME;
return;
}
class PovDisplayUsermod : public Usermod {
@@ -34,6 +34,7 @@ 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()
{
@@ -60,8 +61,8 @@ class RgbRotaryEncoderUsermod : public Usermod
// …then set only the LED pin
_pins[0] = static_cast<byte>(ledIo);
BusConfig busCfg = BusConfig(TYPE_WS2812_RGB, _pins, 0, numLeds, COL_ORDER_GRB, false, 0);
ledBus = new BusDigital(busCfg, WLED_MAX_BUSSES - 1);
busCfg.iType = BusManager::getI(busCfg.type, busCfg.pins, busCfg.driverType); // assign internal bus type and output driver
ledBus = new BusDigital(busCfg);
if (!ledBus->isOk()) {
cleanup();
return;
@@ -79,7 +80,7 @@ class RgbRotaryEncoderUsermod : public Usermod
for (int i = 0; i < currentPos / incrementPerClick - 1; i++) {
ledBus->setPixelColor(i, 0);
}
ledBus->setPixelColor(currentPos / incrementPerClick - 1, colorFromRgbw(currentColors));
ledBus->setPixelColor(currentPos / incrementPerClick - 1, colorFromRgb(currentColors));
for (int i = currentPos / incrementPerClick; i < numLeds; i++) {
ledBus->setPixelColor(i, 0);
}
@@ -95,7 +96,7 @@ class RgbRotaryEncoderUsermod : public Usermod
if (ledMode == 3) {
hsv2rgb((i) / float(numLeds), 1, .25);
}
ledBus->setPixelColor(i, colorFromRgbw(currentColors));
ledBus->setPixelColor(i, colorFromRgb(currentColors));
}
for (int i = currentPos / incrementPerClick; i < numLeds; i++) {
ledBus->setPixelColor(i, 0);
+32 -31
View File
@@ -4,6 +4,7 @@ This usermod is a common place to put various users WLED effects. It lets you
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)
@@ -14,6 +15,17 @@ Multiple Effects can be specified inside this single usermod, as we will illustr
* [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:
@@ -76,12 +88,14 @@ The first line of the code imports the [wled.h](https://github.com/wled/WLED/blo
### 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 returns the static effect which displays no pattern:
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())
return mode_static(); // not a 2D set-up
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
@@ -128,7 +142,7 @@ unsigned dataSize = cols * rows; // SEGLEN (virtual length) is equivalent to vW
```cpp
if (!SEGENV.allocateData(dataSize))
return mode_static(); // allocation failed
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).
@@ -250,20 +264,14 @@ After calculating tmp_row, we now handle rendering the pixels by updating the ac
* `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).
The final piece of this custom effect returns the frame time:
```cpp
}
return FRAMETIME;
}
```
* The first bracket closes the earlier `if ((strip.now - SEGENV.step) >= refresh_ms)` block.
* It ensures that the fire simulation (scrolling, sparking, diffusion, rendering) only runs when enough time has passed since the last update.
* returning the frame time tells WLED how soon this effect wants to be called again.
* `FRAMETIME` is a predefined macro in WLED, typically set to ~16ms, corresponding to ~60 FPS (frames per second).
* Even though the effect logic itself controls when to update based on refresh_ms, WLED will still call this function at roughly FRAMETIME intervals to check whether an update is needed.
* ⚠️ Important: Because the actual frame logic is gated by strip.now - SEGENV.step, returning FRAMETIME here doesnt cause excessive updates — it just keeps the engine responsive. **Also note that an Effect should ALWAYS return FRAMETIME. Not doing so can cause glitches.**
* The final bracket closes the `mode_diffusionfire()` function itself.
* 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**.
@@ -310,13 +318,13 @@ 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 uint16_t sinelon_base(bool dual, bool rainbow=false) {
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) return mode_static();
if (SEGLEN <= 1) FX_FALLBACK_STATIC;
```
* If segment length ≤ 1, theres nothing to animate. Just show static mode.
@@ -396,28 +404,21 @@ This final part of the effect function will fill in the 'trailing' pixels to com
* 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.
Finally, we return the `FRAMETIME`, as with all effect functions:
```cpp
return FRAMETIME;
}
```
* Returns `FRAMETIME` constant to set effect update rate (usually ~16 ms).
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
uint16_t mode_sinelon(void) {
return sinelon_base(false);
void mode_sinelon(void) {
sinelon_base(false);
}
// Calls sinelon_base with dual = false and rainbow = false
uint16_t mode_sinelon_dual(void) {
return sinelon_base(true);
void mode_sinelon_dual(void) {
sinelon_base(true);
}
// Calls sinelon_base with dual = true and rainbow = false
uint16_t mode_sinelon_rainbow(void) {
return sinelon_base(false, true);
void mode_sinelon_rainbow(void) {
sinelon_base(false, true);
}
// Calls sinelon_base with dual = false and rainbow = true
```
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_approximate(fastled_col);
prim_hsv = rgb2hsv(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;
hsv2rgb_rainbow(prim_hsv, fastled_col);
fastled_col = prim_hsv ;
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_approximate(fastled_col);
prim_hsv = rgb2hsv(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;
hsv2rgb_rainbow(prim_hsv, fastled_col);
fastled_col = prim_hsv;
colPri[0] = fastled_col.red;
colPri[1] = fastled_col.green;
colPri[2] = fastled_col.blue;
@@ -284,7 +284,6 @@ 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;
}
@@ -124,7 +124,7 @@ public:
char objKey[14];
bool parsed = false;
if (!requestJSONBufferLock(22)) return false;
if (!requestJSONBufferLock(JSON_LOCK_REMOTE)) return false;
sprintf_P(objKey, PSTR("\"%d\":"), button);
+1 -1
View File
@@ -2,6 +2,6 @@
"name": "animartrix",
"build": { "libArchive": false },
"dependencies": {
"Animartrix": "https://github.com/netmindz/animartrix.git#b172586"
"Animartrix": "https://github.com/netmindz/animartrix.git#81eb09b91c8c9c8c01f8ea442787f8127d56c72f"
}
}
@@ -85,266 +85,214 @@ class ANIMartRIXMod:public ANIMartRIX {
};
ANIMartRIXMod anim;
uint16_t mode_Module_Experiment10() {
void mode_Module_Experiment10() {
anim.initEffect();
anim.Module_Experiment10();
return FRAMETIME;
}
uint16_t mode_Module_Experiment9() {
void mode_Module_Experiment9() {
anim.initEffect();
anim.Module_Experiment9();
return FRAMETIME;
}
uint16_t mode_Module_Experiment8() {
void mode_Module_Experiment8() {
anim.initEffect();
anim.Module_Experiment8();
return FRAMETIME;
}
uint16_t mode_Module_Experiment7() {
void mode_Module_Experiment7() {
anim.initEffect();
anim.Module_Experiment7();
return FRAMETIME;
}
uint16_t mode_Module_Experiment6() {
void mode_Module_Experiment6() {
anim.initEffect();
anim.Module_Experiment6();
return FRAMETIME;
}
uint16_t mode_Module_Experiment5() {
void mode_Module_Experiment5() {
anim.initEffect();
anim.Module_Experiment5();
return FRAMETIME;
}
uint16_t mode_Module_Experiment4() {
void mode_Module_Experiment4() {
anim.initEffect();
anim.Module_Experiment4();
return FRAMETIME;
}
uint16_t mode_Zoom2() {
void mode_Zoom2() {
anim.initEffect();
anim.Zoom2();
return FRAMETIME;
}
uint16_t mode_Module_Experiment3() {
void mode_Module_Experiment3() {
anim.initEffect();
anim.Module_Experiment3();
return FRAMETIME;
}
uint16_t mode_Module_Experiment2() {
void mode_Module_Experiment2() {
anim.initEffect();
anim.Module_Experiment2();
return FRAMETIME;
}
uint16_t mode_Module_Experiment1() {
void mode_Module_Experiment1() {
anim.initEffect();
anim.Module_Experiment1();
return FRAMETIME;
}
uint16_t mode_Parametric_Water() {
void mode_Parametric_Water() {
anim.initEffect();
anim.Parametric_Water();
return FRAMETIME;
}
uint16_t mode_Water() {
void mode_Water() {
anim.initEffect();
anim.Water();
return FRAMETIME;
}
uint16_t mode_Complex_Kaleido_6() {
void mode_Complex_Kaleido_6() {
anim.initEffect();
anim.Complex_Kaleido_6();
return FRAMETIME;
}
uint16_t mode_Complex_Kaleido_5() {
void mode_Complex_Kaleido_5() {
anim.initEffect();
anim.Complex_Kaleido_5();
return FRAMETIME;
}
uint16_t mode_Complex_Kaleido_4() {
void mode_Complex_Kaleido_4() {
anim.initEffect();
anim.Complex_Kaleido_4();
return FRAMETIME;
}
uint16_t mode_Complex_Kaleido_3() {
void mode_Complex_Kaleido_3() {
anim.initEffect();
anim.Complex_Kaleido_3();
return FRAMETIME;
}
uint16_t mode_Complex_Kaleido_2() {
void mode_Complex_Kaleido_2() {
anim.initEffect();
anim.Complex_Kaleido_2();
return FRAMETIME;
}
uint16_t mode_Complex_Kaleido() {
void mode_Complex_Kaleido() {
anim.initEffect();
anim.Complex_Kaleido();
return FRAMETIME;
}
uint16_t mode_SM10() {
void mode_SM10() {
anim.initEffect();
anim.SM10();
return FRAMETIME;
}
uint16_t mode_SM9() {
void mode_SM9() {
anim.initEffect();
anim.SM9();
return FRAMETIME;
}
uint16_t mode_SM8() {
void mode_SM8() {
anim.initEffect();
anim.SM8();
return FRAMETIME;
}
// uint16_t mode_SM7() {
// void mode_SM7() {
// anim.initEffect();
// anim.SM7();
//
// return FRAMETIME;
// }
uint16_t mode_SM6() {
void mode_SM6() {
anim.initEffect();
anim.SM6();
return FRAMETIME;
}
uint16_t mode_SM5() {
void mode_SM5() {
anim.initEffect();
anim.SM5();
return FRAMETIME;
}
uint16_t mode_SM4() {
void mode_SM4() {
anim.initEffect();
anim.SM4();
return FRAMETIME;
}
uint16_t mode_SM3() {
void mode_SM3() {
anim.initEffect();
anim.SM3();
return FRAMETIME;
}
uint16_t mode_SM2() {
void mode_SM2() {
anim.initEffect();
anim.SM2();
return FRAMETIME;
}
uint16_t mode_SM1() {
void mode_SM1() {
anim.initEffect();
anim.SM1();
return FRAMETIME;
}
uint16_t mode_Big_Caleido() {
void mode_Big_Caleido() {
anim.initEffect();
anim.Big_Caleido();
return FRAMETIME;
}
uint16_t mode_RGB_Blobs5() {
void mode_RGB_Blobs5() {
anim.initEffect();
anim.RGB_Blobs5();
return FRAMETIME;
}
uint16_t mode_RGB_Blobs4() {
void mode_RGB_Blobs4() {
anim.initEffect();
anim.RGB_Blobs4();
return FRAMETIME;
}
uint16_t mode_RGB_Blobs3() {
void mode_RGB_Blobs3() {
anim.initEffect();
anim.RGB_Blobs3();
return FRAMETIME;
}
uint16_t mode_RGB_Blobs2() {
void mode_RGB_Blobs2() {
anim.initEffect();
anim.RGB_Blobs2();
return FRAMETIME;
}
uint16_t mode_RGB_Blobs() {
void mode_RGB_Blobs() {
anim.initEffect();
anim.RGB_Blobs();
return FRAMETIME;
}
uint16_t mode_Polar_Waves() {
void mode_Polar_Waves() {
anim.initEffect();
anim.Polar_Waves();
return FRAMETIME;
}
uint16_t mode_Slow_Fade() {
void mode_Slow_Fade() {
anim.initEffect();
anim.Slow_Fade();
return FRAMETIME;
}
uint16_t mode_Zoom() {
void mode_Zoom() {
anim.initEffect();
anim.Zoom();
return FRAMETIME;
}
uint16_t mode_Hot_Blob() {
void mode_Hot_Blob() {
anim.initEffect();
anim.Hot_Blob();
return FRAMETIME;
}
uint16_t mode_Spiralus2() {
void mode_Spiralus2() {
anim.initEffect();
anim.Spiralus2();
return FRAMETIME;
}
uint16_t mode_Spiralus() {
void mode_Spiralus() {
anim.initEffect();
anim.Spiralus();
return FRAMETIME;
}
uint16_t mode_Yves() {
void mode_Yves() {
anim.initEffect();
anim.Yves();
return FRAMETIME;
}
uint16_t mode_Scaledemo1() {
void mode_Scaledemo1() {
anim.initEffect();
anim.Scaledemo1();
return FRAMETIME;
}
uint16_t mode_Lava1() {
void mode_Lava1() {
anim.initEffect();
anim.Lava1();
return FRAMETIME;
}
uint16_t mode_Caleido3() {
void mode_Caleido3() {
anim.initEffect();
anim.Caleido3();
return FRAMETIME;
}
uint16_t mode_Caleido2() {
void mode_Caleido2() {
anim.initEffect();
anim.Caleido2();
return FRAMETIME;
}
uint16_t mode_Caleido1() {
void mode_Caleido1() {
anim.initEffect();
anim.Caleido1();
return FRAMETIME;
}
uint16_t mode_Distance_Experiment() {
void mode_Distance_Experiment() {
anim.initEffect();
anim.Distance_Experiment();
return FRAMETIME;
}
uint16_t mode_Center_Field() {
void mode_Center_Field() {
anim.initEffect();
anim.Center_Field();
return FRAMETIME;
}
uint16_t mode_Waves() {
void mode_Waves() {
anim.initEffect();
anim.Waves();
return FRAMETIME;
}
uint16_t mode_Chasing_Spirals() {
void mode_Chasing_Spirals() {
anim.initEffect();
anim.Chasing_Spirals();
return FRAMETIME;
}
uint16_t mode_Rotating_Blob() {
void mode_Rotating_Blob() {
anim.initEffect();
anim.Rotating_Blob();
return FRAMETIME;
}
@@ -16,6 +16,10 @@
// It can be configured to load auto saved preset at startup,
// during the first `loop()`.
//
// By default it will not save the state if an unmodified preset
// is selected (to not duplicate it). You can change this behaviour
// by setting autoSaveIgnorePresets=false
//
// AutoSaveUsermod is standalone, but if FourLineDisplayUsermod
// is installed, it will notify the user of the saved changes.
@@ -49,6 +53,8 @@ class AutoSaveUsermod : public Usermod {
bool applyAutoSaveOnBoot = false; // do we load auto-saved preset on boot?
#endif
bool autoSaveIgnorePresets = true; // ignore by default to not duplicate presets
// If we've detected the need to auto save, this will be non zero.
unsigned long autoSaveAfter = 0;
@@ -68,6 +74,7 @@ class AutoSaveUsermod : public Usermod {
static const char _autoSaveAfterSec[];
static const char _autoSavePreset[];
static const char _autoSaveApplyOnBoot[];
static const char _autoSaveIgnorePresets[];
void inline saveSettings() {
char presetNameBuffer[PRESET_NAME_BUFFER_SIZE];
@@ -122,7 +129,8 @@ class AutoSaveUsermod : public Usermod {
void loop() {
static unsigned long lastRun = 0;
unsigned long now = millis();
if (!autoSaveAfterSec || !enabled || currentPreset>0 || (strip.isUpdating() && now - lastRun < 240)) return; // setting 0 as autosave seconds disables autosave
if (!autoSaveAfterSec || !enabled || (autoSaveIgnorePresets && currentPreset>0) || (strip.isUpdating() && now - lastRun < 240)) return; // setting 0 as autosave seconds disables autosave
lastRun = now;
uint8_t currentMode = strip.getMainSegment().mode;
uint8_t currentPalette = strip.getMainSegment().palette;
@@ -219,10 +227,11 @@ class AutoSaveUsermod : public Usermod {
void addToConfig(JsonObject& root) {
// we add JSON object: {"Autosave": {"autoSaveAfterSec": 10, "autoSavePreset": 99}}
JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname
top[FPSTR(_autoSaveEnabled)] = enabled;
top[FPSTR(_autoSaveAfterSec)] = autoSaveAfterSec; // usermodparam
top[FPSTR(_autoSavePreset)] = autoSavePreset; // usermodparam
top[FPSTR(_autoSaveApplyOnBoot)] = applyAutoSaveOnBoot;
top[FPSTR(_autoSaveEnabled)] = enabled;
top[FPSTR(_autoSaveAfterSec)] = autoSaveAfterSec; // usermodparam
top[FPSTR(_autoSavePreset)] = autoSavePreset; // usermodparam
top[FPSTR(_autoSaveApplyOnBoot)] = applyAutoSaveOnBoot;
top[FPSTR(_autoSaveIgnorePresets)] = autoSaveIgnorePresets;
DEBUG_PRINTLN(F("Autosave config saved."));
}
@@ -245,12 +254,13 @@ class AutoSaveUsermod : public Usermod {
return false;
}
enabled = top[FPSTR(_autoSaveEnabled)] | enabled;
autoSaveAfterSec = top[FPSTR(_autoSaveAfterSec)] | autoSaveAfterSec;
autoSaveAfterSec = (uint16_t) min(3600,max(10,(int)autoSaveAfterSec)); // bounds checking
autoSavePreset = top[FPSTR(_autoSavePreset)] | autoSavePreset;
autoSavePreset = (uint8_t) min(250,max(100,(int)autoSavePreset)); // bounds checking
applyAutoSaveOnBoot = top[FPSTR(_autoSaveApplyOnBoot)] | applyAutoSaveOnBoot;
enabled = top[FPSTR(_autoSaveEnabled)] | enabled;
autoSaveAfterSec = top[FPSTR(_autoSaveAfterSec)] | autoSaveAfterSec;
autoSaveAfterSec = (uint16_t) min(3600,max(10,(int)autoSaveAfterSec)); // bounds checking
autoSavePreset = top[FPSTR(_autoSavePreset)] | autoSavePreset;
autoSavePreset = (uint8_t) min(250,max(100,(int)autoSavePreset)); // bounds checking
applyAutoSaveOnBoot = top[FPSTR(_autoSaveApplyOnBoot)] | applyAutoSaveOnBoot;
autoSaveIgnorePresets = top[FPSTR(_autoSaveIgnorePresets)] | autoSaveIgnorePresets;
DEBUG_PRINT(FPSTR(_name));
DEBUG_PRINTLN(F(" config (re)loaded."));
@@ -268,11 +278,12 @@ class AutoSaveUsermod : public Usermod {
};
// strings to reduce flash memory usage (used more than twice)
const char AutoSaveUsermod::_name[] PROGMEM = "Autosave";
const char AutoSaveUsermod::_autoSaveEnabled[] PROGMEM = "enabled";
const char AutoSaveUsermod::_autoSaveAfterSec[] PROGMEM = "autoSaveAfterSec";
const char AutoSaveUsermod::_autoSavePreset[] PROGMEM = "autoSavePreset";
const char AutoSaveUsermod::_autoSaveApplyOnBoot[] PROGMEM = "autoSaveApplyOnBoot";
const char AutoSaveUsermod::_name[] PROGMEM = "Autosave";
const char AutoSaveUsermod::_autoSaveEnabled[] PROGMEM = "enabled";
const char AutoSaveUsermod::_autoSaveAfterSec[] PROGMEM = "autoSaveAfterSec";
const char AutoSaveUsermod::_autoSavePreset[] PROGMEM = "autoSavePreset";
const char AutoSaveUsermod::_autoSaveApplyOnBoot[] PROGMEM = "autoSaveApplyOnBoot";
const char AutoSaveUsermod::_autoSaveIgnorePresets[] PROGMEM = "autoSaveIgnorePresets";
static AutoSaveUsermod autosave;
REGISTER_USERMOD(autosave);
@@ -749,12 +749,12 @@ bool FourLineDisplayUsermod::handleButton(uint8_t b) {
yield();
if (!enabled
|| b // button 0 only
|| 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) {
|| 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) {
return false;
}
@@ -401,19 +401,19 @@ void RotaryEncoderUIUsermod::sortModesAndPalettes() {
re_sortModes(modes_qstrings, modes_alpha_indexes, strip.getModeCount(), MODE_SORT_SKIP_COUNT);
DEBUG_PRINT(F("Sorting palettes: ")); DEBUG_PRINT(getPaletteCount()); DEBUG_PRINT('/'); DEBUG_PRINTLN(customPalettes.size());
palettes_qstrings = re_findModeStrings(JSON_palette_names, getPaletteCount());
palettes_alpha_indexes = re_initIndexArray(getPaletteCount());
palettes_qstrings = re_findModeStrings(JSON_palette_names, getPaletteCount()); // allocates memory for all palette names
palettes_alpha_indexes = re_initIndexArray(getPaletteCount()); // allocates memory for all palette indexes
if (customPalettes.size()) {
for (int i=0; i<customPalettes.size(); i++) {
palettes_alpha_indexes[getPaletteCount()-customPalettes.size()+i] = 255-i;
palettes_qstrings[getPaletteCount()-customPalettes.size()+i] = PSTR("~Custom~");
palettes_alpha_indexes[FIXED_PALETTE_COUNT+i] = 255-i;
palettes_qstrings[FIXED_PALETTE_COUNT+i] = PSTR("~Custom~");
}
}
// How many palette names start with '*' and should not be sorted?
// (Also skipping the first one, 'Default').
int skipPaletteCount = 1;
while (pgm_read_byte_near(palettes_qstrings[skipPaletteCount]) == '*') skipPaletteCount++;
re_sortModes(palettes_qstrings, palettes_alpha_indexes, getPaletteCount()-customPalettes.size(), skipPaletteCount);
int skipPaletteCount = 1; // could use DYNAMIC_PALETTE_COUNT instead
while (pgm_read_byte_near(palettes_qstrings[skipPaletteCount]) == '*') skipPaletteCount++; // legacy code
re_sortModes(palettes_qstrings, palettes_alpha_indexes, FIXED_PALETTE_COUNT, skipPaletteCount); // only sort fixed palettes (skip dynamic)
}
byte *RotaryEncoderUIUsermod::re_initIndexArray(int numModes) {
+1317 -1120
View File
File diff suppressed because it is too large Load Diff
+59 -68
View File
@@ -18,7 +18,7 @@
#include <vector>
#include "wled.h"
#include "colors.h"
#ifdef WLED_DEBUG
// enable additional debug output
#if defined(WLED_DEBUG_HOST)
@@ -38,10 +38,6 @@
#define DEBUGFX_PRINTF_P(x...)
#endif
#define FASTLED_INTERNAL //remove annoying pragma messages
#define USE_GET_MILLISECOND_TIMER
#include "FastLED.h"
#define DEFAULT_BRIGHTNESS (uint8_t)127
#define DEFAULT_MODE (uint8_t)0
#define DEFAULT_SPEED (uint8_t)128
@@ -58,11 +54,6 @@
#define MAX(a,b) ((a)>(b)?(a):(b))
#endif
//color mangling macros
#ifndef RGBW32
#define RGBW32(r,g,b,w) (uint32_t((byte(w) << 24) | (byte(r) << 16) | (byte(g) << 8) | (byte(b))))
#endif
extern bool realtimeRespectLedMaps; // used in getMappedPixelIndex()
extern byte realtimeMode; // used in getMappedPixelIndex()
@@ -310,6 +301,7 @@ extern byte realtimeMode; // used in getMappedPixelIndex()
#define FX_MODE_2DFIRENOISE 149
#define FX_MODE_2DSQUAREDSWIRL 150
// #define FX_MODE_2DFIRE2012 151
#define FX_MODE_PACMAN 151 // gap fill (non-SR). Do NOT renumber; SR-ID range must remain stable.
#define FX_MODE_2DDNA 152
#define FX_MODE_2DMATRIX 153
#define FX_MODE_2DMETABALLS 154
@@ -378,36 +370,38 @@ extern byte realtimeMode; // used in getMappedPixelIndex()
#define FX_MODE_PS1DSONICBOOM 215
#define FX_MODE_PS1DSPRINGY 216
#define FX_MODE_PARTICLEGALAXY 217
#define MODE_COUNT 218
#define FX_MODE_COLORCLOUDS 218
#define FX_MODE_SLOW_TRANSITION 219
#define MODE_COUNT 220
#define BLEND_STYLE_FADE 0x00 // universal
#define BLEND_STYLE_FAIRY_DUST 0x01 // universal
#define BLEND_STYLE_SWIPE_RIGHT 0x02 // 1D or 2D
#define BLEND_STYLE_SWIPE_LEFT 0x03 // 1D or 2D
#define BLEND_STYLE_OUTSIDE_IN 0x04 // 1D or 2D
#define BLEND_STYLE_INSIDE_OUT 0x05 // 1D or 2D
#define BLEND_STYLE_SWIPE_UP 0x06 // 2D
#define BLEND_STYLE_SWIPE_DOWN 0x07 // 2D
#define BLEND_STYLE_OPEN_H 0x08 // 2D
#define BLEND_STYLE_OPEN_V 0x09 // 2D
#define BLEND_STYLE_SWIPE_TL 0x0A // 2D
#define BLEND_STYLE_SWIPE_TR 0x0B // 2D
#define BLEND_STYLE_SWIPE_BR 0x0C // 2D
#define BLEND_STYLE_SWIPE_BL 0x0D // 2D
#define BLEND_STYLE_CIRCULAR_OUT 0x0E // 2D
#define BLEND_STYLE_CIRCULAR_IN 0x0F // 2D
#define TRANSITION_FADE 0x00 // universal
#define TRANSITION_FAIRY_DUST 0x01 // universal
#define TRANSITION_SWIPE_RIGHT 0x02 // 1D or 2D
#define TRANSITION_SWIPE_LEFT 0x03 // 1D or 2D
#define TRANSITION_OUTSIDE_IN 0x04 // 1D or 2D
#define TRANSITION_INSIDE_OUT 0x05 // 1D or 2D
#define TRANSITION_SWIPE_UP 0x06 // 2D
#define TRANSITION_SWIPE_DOWN 0x07 // 2D
#define TRANSITION_OPEN_H 0x08 // 2D
#define TRANSITION_OPEN_V 0x09 // 2D
#define TRANSITION_SWIPE_TL 0x0A // 2D
#define TRANSITION_SWIPE_TR 0x0B // 2D
#define TRANSITION_SWIPE_BR 0x0C // 2D
#define TRANSITION_SWIPE_BL 0x0D // 2D
#define TRANSITION_CIRCULAR_OUT 0x0E // 2D
#define TRANSITION_CIRCULAR_IN 0x0F // 2D
// as there are many push variants to optimise if statements they are groupped together
#define BLEND_STYLE_PUSH_RIGHT 0x10 // 1D or 2D (& 0b00010000)
#define BLEND_STYLE_PUSH_LEFT 0x11 // 1D or 2D (& 0b00010000)
#define BLEND_STYLE_PUSH_UP 0x12 // 2D (& 0b00010000)
#define BLEND_STYLE_PUSH_DOWN 0x13 // 2D (& 0b00010000)
#define BLEND_STYLE_PUSH_TL 0x14 // 2D (& 0b00010000)
#define BLEND_STYLE_PUSH_TR 0x15 // 2D (& 0b00010000)
#define BLEND_STYLE_PUSH_BR 0x16 // 2D (& 0b00010000)
#define BLEND_STYLE_PUSH_BL 0x17 // 2D (& 0b00010000)
#define BLEND_STYLE_PUSH_MASK 0x10
#define BLEND_STYLE_COUNT 18
#define TRANSITION_PUSH_RIGHT 0x10 // 1D or 2D (& 0b00010000)
#define TRANSITION_PUSH_LEFT 0x11 // 1D or 2D (& 0b00010000)
#define TRANSITION_PUSH_UP 0x12 // 2D (& 0b00010000)
#define TRANSITION_PUSH_DOWN 0x13 // 2D (& 0b00010000)
#define TRANSITION_PUSH_TL 0x14 // 2D (& 0b00010000)
#define TRANSITION_PUSH_TR 0x15 // 2D (& 0b00010000)
#define TRANSITION_PUSH_BR 0x16 // 2D (& 0b00010000)
#define TRANSITION_PUSH_BL 0x17 // 2D (& 0b00010000)
#define TRANSITION_PUSH_MASK 0x10
#define TRANSITION_COUNT 18
typedef enum mapping1D2D {
@@ -419,10 +413,12 @@ typedef enum mapping1D2D {
} mapping1D2D_t;
class WS2812FX;
class FontManager;
// segment, 76 bytes
class Segment {
public:
friend class FontManager; // Allow FontManager to access protected members
uint32_t colors[NUM_COLORS];
uint16_t start; // start index / start X coordinate 2D (left)
uint16_t stop; // stop index / stop X coordinate 2D (right); segment is invalid if stop == 0
@@ -459,13 +455,11 @@ class Segment {
bool check1 : 1; // checkmark 1
bool check2 : 1; // checkmark 2
bool check3 : 1; // checkmark 3
//uint8_t blendMode : 4; // segment blending modes: top, bottom, add, subtract, difference, multiply, divide, lighten, darken, screen, overlay, hardlight, softlight, dodge, burn
};
uint8_t blendMode; // segment blending modes: top, bottom, add, subtract, difference, multiply, divide, lighten, darken, screen, overlay, hardlight, softlight, dodge, burn
uint8_t blendMode; // segment blending modes: top, bottom, add, subtract, difference, average, multiply, divide, lighten, darken, screen, overlay, hardlight, softlight, dodge, burn, stencil
char *name; // segment name
// runtime data
mutable unsigned long next_time; // millis() of next update
mutable uint32_t step; // custom "step" var
mutable uint32_t call; // call counter
mutable uint16_t aux0; // custom var
@@ -520,7 +514,7 @@ class Segment {
, _start(millis())
, _colors{0,0,0}
#ifndef WLED_SAVE_RAM
, _palT(CRGBPalette16(CRGB::Black))
, _palT(CRGBPalette16())
#endif
, _dur(dur)
, _progress(0)
@@ -537,7 +531,7 @@ class Segment {
protected:
inline static void addUsedSegmentData(int len) { Segment::_usedSegmentData += len; }
inline static void addUsedSegmentData(int len) { Segment::_usedSegmentData = max(0, int(Segment::_usedSegmentData) + len); } // clamp negative results to 0
inline uint32_t *getPixels() const { return pixels; }
inline void setPixelColorRaw(unsigned i, uint32_t c) const { pixels[i] = c; }
@@ -547,7 +541,7 @@ class Segment {
inline uint32_t getPixelColorXYRaw(unsigned x, unsigned y) const { auto XY = [](unsigned X, unsigned Y){ return X + Y*Segment::vWidth(); }; return pixels[XY(x,y)]; };
#endif
void resetIfRequired(); // sets all SEGENV variables to 0 and clears data buffer
CRGBPalette16 &loadPalette(CRGBPalette16 &tgt, uint8_t pal);
void loadPalette(CRGBPalette16 &tgt, uint8_t pal);
// transition functions
void stopTransition(); // ends transition mode by destroying transition structure (does nothing if not in transition)
@@ -559,9 +553,9 @@ class Segment {
inline uint16_t progress() const { return isInTransition() ? _t->_progress : 0xFFFFU; } // relies on handleTransition()/updateTransitionProgress() to update progression variable
inline Segment *getOldSegment() const { return isInTransition() ? _t->_oldSegment : nullptr; }
inline static void modeBlend(bool blend) { Segment::_modeBlend = blend; }
inline static void modeBlend(bool blend) { Segment::_modeBlend = blend; } // for isPreviousMode()
inline static void setClippingRect(int startX, int stopX, int startY = 0, int stopY = 1) { _clipStart = startX; _clipStop = stopX; _clipStartY = startY; _clipStopY = stopY; };
inline static bool isPreviousMode() { return Segment::_modeBlend; } // needed for determining CCT/opacity during non-BLEND_STYLE_FADE transition
inline static bool isPreviousMode() { return Segment::_modeBlend; } // needed for determining CCT/opacity during non-TRANSITION_FADE transition
static void handleRandomPalette();
@@ -591,7 +585,6 @@ class Segment {
, check3(false)
, blendMode(0)
, name(nullptr)
, next_time(0)
, step(0)
, call(0)
, aux0(0)
@@ -625,6 +618,7 @@ class Segment {
DEBUGFX_PRINTLN();
#endif
clearName();
stopTransition(); // deallocate "_t" (transition) and with it "_segOld" note: _segOld has _t=null, see copy constructor
#ifdef WLED_ENABLE_GIF
endImagePlayback(this);
#endif
@@ -761,8 +755,8 @@ class Segment {
{ addPixelColorXY(x, y, RGBW32(r,g,b,w), preserveCR); }
inline void addPixelColorXY(int x, int y, CRGB c, bool preserveCR = true) const { addPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0), preserveCR); }
inline void fadePixelColorXY(uint16_t x, uint16_t y, uint8_t fade) const { setPixelColorXY(x, y, color_fade(getPixelColorXY(x,y), fade, true)); }
inline void blurCols(fract8 blur_amount, bool smear = false) const { blur2D(0, blur_amount, smear); } // blur all columns (50% faster than full 2D blur)
inline void blurRows(fract8 blur_amount, bool smear = false) const { blur2D(blur_amount, 0, smear); } // blur all rows (50% faster than full 2D blur)
inline void blurCols(uint8_t blur_amount, bool smear = false) const { blur2D(0, blur_amount, smear); } // blur all columns (50% faster than full 2D blur)
inline void blurRows(uint8_t blur_amount, bool smear = false) const { blur2D(blur_amount, 0, smear); } // blur all rows (50% faster than full 2D blur)
//void box_blur(unsigned r = 1U, bool smear = false); // 2D box blur
void blur2D(uint8_t blur_x, uint8_t blur_y, bool smear = false) const;
void moveX(int delta, bool wrap = false) const;
@@ -771,12 +765,10 @@ class Segment {
void drawCircle(uint16_t cx, uint16_t cy, uint8_t radius, uint32_t c, bool soft = false) const;
void fillCircle(uint16_t cx, uint16_t cy, uint8_t radius, uint32_t c, bool soft = false) const;
void drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint32_t c, bool soft = false) const;
void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, uint32_t color, uint32_t col2 = 0, int8_t rotate = 0) const;
void wu_pixel(uint32_t x, uint32_t y, CRGB c) const;
inline void drawCircle(uint16_t cx, uint16_t cy, uint8_t radius, CRGB c, bool soft = false) const { drawCircle(cx, cy, radius, RGBW32(c.r,c.g,c.b,0), soft); }
inline void fillCircle(uint16_t cx, uint16_t cy, uint8_t radius, CRGB c, bool soft = false) const { fillCircle(cx, cy, radius, RGBW32(c.r,c.g,c.b,0), soft); }
inline void drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, CRGB c, bool soft = false) const { drawLine(x0, y0, x1, y1, RGBW32(c.r,c.g,c.b,0), soft); } // automatic inline
inline void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, CRGB c, CRGB c2 = CRGB::Black, int8_t rotate = 0) const { drawCharacter(chr, x, y, w, h, RGBW32(c.r,c.g,c.b,0), RGBW32(c2.r,c2.g,c2.b,0), rotate); } // automatic inline
inline void fill_solid(CRGB c) const { fill(RGBW32(c.r,c.g,c.b,0)); }
#else
inline bool is2D() const { return false; }
@@ -790,18 +782,18 @@ class Segment {
inline void setPixelColorXY(float x, float y, byte r, byte g, byte b, byte w = 0, bool aa = true) { setPixelColor(x, RGBW32(r,g,b,w), aa); }
inline void setPixelColorXY(float x, float y, CRGB c, bool aa = true) const { setPixelColor(x, RGBW32(c.r,c.g,c.b,0), aa); }
#endif
inline bool isPixelXYClipped(int x, int y) const { return isPixelClipped(x); }
inline uint32_t getPixelColorXY(int x, int y) const { return getPixelColor(x); }
inline void blendPixelColorXY(uint16_t x, uint16_t y, uint32_t c, uint8_t blend) const { blendPixelColor(x, c, blend); }
inline void blendPixelColorXY(uint16_t x, uint16_t y, CRGB c, uint8_t blend) const { blendPixelColor(x, RGBW32(c.r,c.g,c.b,0), blend); }
inline void addPixelColorXY(int x, int y, uint32_t color, bool saturate = false) const { addPixelColor(x, color, saturate); }
inline void addPixelColorXY(int x, int y, byte r, byte g, byte b, byte w = 0, bool saturate = false) const { addPixelColor(x, RGBW32(r,g,b,w), saturate); }
inline void addPixelColorXY(int x, int y, CRGB c, bool saturate = false) const { addPixelColor(x, RGBW32(c.r,c.g,c.b,0), saturate); }
inline void fadePixelColorXY(uint16_t x, uint16_t y, uint8_t fade) const { fadePixelColor(x, fade); }
//inline void box_blur(unsigned i, bool vertical, fract8 blur_amount) {}
inline bool isPixelXYClipped(int x, int y) { return isPixelClipped(x); }
inline uint32_t getPixelColorXY(int x, int y) { return getPixelColor(x); }
inline void blendPixelColorXY(uint16_t x, uint16_t y, uint32_t c, uint8_t blend) { blendPixelColor(x, c, blend); }
inline void blendPixelColorXY(uint16_t x, uint16_t y, CRGB c, uint8_t blend) { blendPixelColor(x, RGBW32(c.r,c.g,c.b,0), blend); }
inline void addPixelColorXY(int x, int y, uint32_t color, bool preserveCR = true) { addPixelColor(x, color, preserveCR); }
inline void addPixelColorXY(int x, int y, byte r, byte g, byte b, byte w = 0, bool preserveCR = true) { addPixelColor(x, RGBW32(r,g,b,w), preserveCR); }
inline void addPixelColorXY(int x, int y, CRGB c, bool preserveCR = true) { addPixelColor(x, RGBW32(c.r,c.g,c.b,0), preserveCR); }
inline void fadePixelColorXY(uint16_t x, uint16_t y, uint8_t fade) { fadePixelColor(x, fade); }
//inline void box_blur(unsigned i, bool vertical, uint8_t blur_amount) {}
inline void blur2D(uint8_t blur_x, uint8_t blur_y, bool smear = false) {}
inline void blurCols(fract8 blur_amount, bool smear = false) { blur(blur_amount, smear); } // blur all columns (50% faster than full 2D blur)
inline void blurRows(fract8 blur_amount, bool smear = false) {}
inline void blurRows(uint8_t blur_amount, bool smear = false) {}
inline void blurCols(uint8_t blur_amount, bool smear = false) {}
inline void moveX(int delta, bool wrap = false) {}
inline void moveY(int delta, bool wrap = false) {}
inline void move(uint8_t dir, uint8_t delta, bool wrap = false) {}
@@ -811,8 +803,6 @@ class Segment {
inline void fillCircle(uint16_t cx, uint16_t cy, uint8_t radius, CRGB c, bool soft = false) {}
inline void drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint32_t c, bool soft = false) {}
inline void drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, CRGB c, bool soft = false) {}
inline void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, uint32_t color, uint32_t = 0, int8_t = 0) {}
inline void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, CRGB c, CRGB c2, int8_t rotate = 0) {}
inline void wu_pixel(uint32_t x, uint32_t y, CRGB c) {}
#endif
friend class WS2812FX;
@@ -822,19 +812,18 @@ class Segment {
// main "strip" class (108 bytes)
class WS2812FX {
typedef uint16_t (*mode_ptr)(); // pointer to mode function
typedef void (*mode_ptr)(); // pointer to mode function
typedef void (*show_callback)(); // pre show callback
typedef struct ModeData {
uint8_t _id; // mode (effect) id
mode_ptr _fcn; // mode (effect) function
const char *_data; // mode (effect) name and its UI control data
ModeData(uint8_t id, uint16_t (*fcn)(void), const char *data) : _id(id), _fcn(fcn), _data(data) {}
ModeData(uint8_t id, void (*fcn)(void), const char *data) : _id(id), _fcn(fcn), _data(data) {}
} mode_data_t;
public:
WS2812FX() :
paletteBlend(0),
now(millis()),
timebase(0),
isMatrix(false),
@@ -891,6 +880,7 @@ class WS2812FX {
printSize(), // prints memory usage for strip components
#endif
finalizeInit(), // initialises strip components
updatePixelBuffer(), // (re)allocate memory for _pixels[]
service(), // executes effect functions when due and calls strip.show()
setCCT(uint16_t k), // sets global CCT (either in relative 0-255 value or in K)
setBrightness(uint8_t b, bool direct = false), // sets strip brightness
@@ -936,7 +926,7 @@ class WS2812FX {
inline bool isSuspended() const { return _suspend; } // returns true if strip.service() execution is suspended
inline bool needsUpdate() const { return _triggered; } // returns true if strip received a trigger() request
uint8_t paletteBlend;
// uint8_t paletteBlend; // obsolete - use global paletteBlend instead of strip.paletteBlend
uint8_t getActiveSegmentsNum() const;
uint8_t getFirstSelectedSegId() const;
uint8_t getLastActiveSegmentId() const;
@@ -965,7 +955,8 @@ class WS2812FX {
};
unsigned long now, timebase;
inline uint32_t getPixelColor(unsigned n) const { return (n < getLengthTotal()) ? _pixels[n] : 0; } // returns color of pixel n
inline uint32_t getPixelColor(unsigned n) const { return (getMappedPixelIndex(n) < getLengthTotal()) ? _pixels[n] : 0; } // returns color of pixel n, black if out of (mapped) bounds
inline uint32_t getPixelColorNoMap(unsigned n) const { return (n < getLengthTotal()) ? _pixels[n] : 0; } // ignores mapping table
inline uint32_t getLastShow() const { return _lastShow; } // returns millis() timestamp of last strip.show() call
const char *getModeData(unsigned id = 0) const { return (id && id < _modeCount) ? _modeData[id] : PSTR("Solid"); }
+12 -52
View File
@@ -74,7 +74,7 @@ void WS2812FX::setUpMatrix() {
size_t gapSize = 0;
int8_t *gapTable = nullptr;
if (isFile && requestJSONBufferLock(20)) {
if (isFile && requestJSONBufferLock(JSON_LOCK_LEDGAP)) {
DEBUG_PRINT(F("Reading LED gap from "));
DEBUG_PRINTLN(fileName);
// read the array into global JSON buffer
@@ -120,6 +120,9 @@ void WS2812FX::setUpMatrix() {
for (unsigned i=0; i<customMappingSize; i++) {
if (!(i%Segment::maxWidth)) DEBUG_PRINTLN();
DEBUG_PRINTF_P(PSTR("%4d,"), customMappingTable[i]);
#if defined(CONFIG_IDF_TARGET_ESP32S2)
delay(1); // on S2 the CDC output can crash without a delay
#endif
}
DEBUG_PRINTLN();
#endif
@@ -146,14 +149,14 @@ void WS2812FX::setUpMatrix() {
// pixel is clipped if it falls outside clipping range
// if clipping start > stop the clipping range is inverted
bool Segment::isPixelXYClipped(int x, int y) const {
if (blendingStyle != BLEND_STYLE_FADE && isInTransition() && _clipStart != _clipStop) {
if (blendingStyle != TRANSITION_FADE && isInTransition() && _clipStart != _clipStop) {
const bool invertX = _clipStart > _clipStop;
const bool invertY = _clipStartY > _clipStopY;
const int cStartX = invertX ? _clipStop : _clipStart;
const int cStopX = invertX ? _clipStart : _clipStop;
const int cStartY = invertY ? _clipStopY : _clipStartY;
const int cStopY = invertY ? _clipStartY : _clipStopY;
if (blendingStyle == BLEND_STYLE_FAIRY_DUST) {
if (blendingStyle == TRANSITION_FAIRY_DUST) {
const unsigned width = cStopX - cStartX; // assumes full segment width (faster than virtualWidth())
const unsigned len = width * (cStopY - cStartY); // assumes full segment height (faster than virtualHeight())
if (len < 2) return false;
@@ -161,10 +164,10 @@ bool Segment::isPixelXYClipped(int x, int y) const {
const unsigned pos = (shuffled * 0xFFFFU) / len;
return progress() <= pos;
}
if (blendingStyle == BLEND_STYLE_CIRCULAR_IN || blendingStyle == BLEND_STYLE_CIRCULAR_OUT) {
if (blendingStyle == TRANSITION_CIRCULAR_IN || blendingStyle == TRANSITION_CIRCULAR_OUT) {
const int cx = (cStopX-cStartX+1) / 2;
const int cy = (cStopY-cStartY+1) / 2;
const bool out = (blendingStyle == BLEND_STYLE_CIRCULAR_OUT);
const bool out = (blendingStyle == TRANSITION_CIRCULAR_OUT);
const unsigned prog = out ? progress() : 0xFFFFU - progress();
int radius2 = max(cx, cy) * prog / 0xFFFF;
radius2 = 2 * radius2 * radius2;
@@ -176,7 +179,7 @@ bool Segment::isPixelXYClipped(int x, int y) const {
}
bool xInside = (x >= cStartX && x < cStopX); if (invertX) xInside = !xInside;
bool yInside = (y >= cStartY && y < cStopY); if (invertY) yInside = !yInside;
const bool clip = blendingStyle == BLEND_STYLE_OUTSIDE_IN ? xInside || yInside : xInside && yInside;
const bool clip = blendingStyle == TRANSITION_OUTSIDE_IN ? xInside || yInside : xInside && yInside;
return !clip;
}
return false;
@@ -559,51 +562,6 @@ void Segment::drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint3
}
}
#include "src/font/console_font_4x6.h"
#include "src/font/console_font_5x8.h"
#include "src/font/console_font_5x12.h"
#include "src/font/console_font_6x8.h"
#include "src/font/console_font_7x9.h"
// draws a raster font character on canvas
// only supports: 4x6=24, 5x8=40, 5x12=60, 6x8=48 and 7x9=63 fonts ATM
void Segment::drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, uint32_t color, uint32_t col2, int8_t rotate) const {
if (!isActive()) return; // not active
if (chr < 32 || chr > 126) return; // only ASCII 32-126 supported
chr -= 32; // align with font table entries
const int font = w*h;
// if col2 == BLACK then use currently selected palette for gradient otherwise create gradient from color and col2
CRGBPalette16 grad = col2 ? CRGBPalette16(CRGB(color), CRGB(col2)) : SEGPALETTE; // selected palette as gradient
for (int i = 0; i<h; i++) { // character height
uint8_t bits = 0;
switch (font) {
case 24: bits = pgm_read_byte_near(&console_font_4x6[(chr * h) + i]); break; // 4x6 font
case 40: bits = pgm_read_byte_near(&console_font_5x8[(chr * h) + i]); break; // 5x8 font
case 48: bits = pgm_read_byte_near(&console_font_6x8[(chr * h) + i]); break; // 6x8 font
case 63: bits = pgm_read_byte_near(&console_font_7x9[(chr * h) + i]); break; // 7x9 font
case 60: bits = pgm_read_byte_near(&console_font_5x12[(chr * h) + i]); break; // 5x12 font
default: return;
}
CRGBW c = ColorFromPalette(grad, (i+1)*255/h, 255, LINEARBLEND_NOWRAP); // NOBLEND is faster
for (int j = 0; j<w; j++) { // character width
int x0, y0;
switch (rotate) {
case -1: x0 = x + (h-1) - i; y0 = y + (w-1) - j; break; // -90 deg
case -2:
case 2: x0 = x + j; y0 = y + (h-1) - i; break; // 180 deg
case 1: x0 = x + i; y0 = y + j; break; // +90 deg
default: x0 = x + (w-1) - j; y0 = y + i; break; // no rotation
}
if (x0 < 0 || x0 >= (int)vWidth() || y0 < 0 || y0 >= (int)vHeight()) continue; // drawing off-screen
if (((bits>>(j+(8-w))) & 0x01)) { // bit set
setPixelColorXYRaw(x0, y0, c.color32);
}
}
}
}
#define WU_WEIGHT(a,b) ((uint8_t) (((a)*(b)+(a)+(b))>>8))
void Segment::wu_pixel(uint32_t x, uint32_t y, CRGB c) const { //awesome wu_pixel procedure by reddit u/sutaburosu
if (!isActive()) return; // not active
@@ -626,4 +584,6 @@ void Segment::wu_pixel(uint32_t x, uint32_t y, CRGB c) const { //awesome wu
}
#undef WU_WEIGHT
#endif // WLED_DISABLE_2D
#endif // WLED_DISABLE_2D
+273 -179
View File
@@ -11,6 +11,7 @@
*/
#include "wled.h"
#include "FXparticleSystem.h" // TODO: better define the required function (mem service) in FX.h?
#include "colors.h"
/*
Custom per-LED mapping has moved!
@@ -43,7 +44,7 @@ unsigned Segment::_vLength = 0;
unsigned Segment::_vWidth = 0;
unsigned Segment::_vHeight = 0;
uint32_t Segment::_currentColors[NUM_COLORS] = {0,0,0};
CRGBPalette16 Segment::_currentPalette = CRGBPalette16(CRGB::Black);
CRGBPalette16 Segment::_currentPalette = CRGBPalette16();
CRGBPalette16 Segment::_randomPalette = generateRandomPalette(); // was CRGBPalette16(DEFAULT_COLOR);
CRGBPalette16 Segment::_newRandomPalette = generateRandomPalette(); // was CRGBPalette16(DEFAULT_COLOR);
uint16_t Segment::_lastPaletteChange = 0; // in seconds; perhaps it should be per segment
@@ -97,15 +98,15 @@ Segment& Segment::operator= (const Segment &orig) {
if (this != &orig) {
// clean destination
if (name) { p_free(name); name = nullptr; }
if (_t) stopTransition(); // also erases _t
stopTransition(); // delete _t
deallocateData();
p_free(pixels);
pixels = nullptr;
// copy source
memcpy((void*)this, (void*)&orig, sizeof(Segment));
// erase pointers to allocated data
data = nullptr;
_dataLen = 0;
pixels = nullptr;
if (!stop) return *this; // nothing to do if segment is inactive/invalid
// copy source data
if (orig.pixels) {
@@ -130,7 +131,7 @@ Segment& Segment::operator= (Segment &&orig) noexcept {
//DEBUG_PRINTF_P(PSTR("-- Moving segment: %p -> %p\n"), &orig, this);
if (this != &orig) {
if (name) { p_free(name); name = nullptr; } // free old name
if (_t) stopTransition(); // also erases _t
stopTransition(); // delete _t
deallocateData(); // free old runtime data
p_free(pixels); // free old pixel buffer
// move source data
@@ -217,25 +218,32 @@ void Segment::resetIfRequired() {
DEBUG_PRINTF_P(PSTR("-- Segment %p reset, data cleared\n"), this);
}
if (pixels) for (size_t i = 0; i < length(); i++) pixels[i] = BLACK; // clear pixel buffer
next_time = 0; step = 0; call = 0; aux0 = 0; aux1 = 0;
step = 0; call = 0; aux0 = 0; aux1 = 0;
reset = false;
#ifdef WLED_ENABLE_GIF
endImagePlayback(this);
#endif
}
CRGBPalette16 &Segment::loadPalette(CRGBPalette16 &targetPalette, uint8_t pal) {
// there is one randomy generated palette (1) followed by 4 palettes created from segment colors (2-5)
void Segment::loadPalette(CRGBPalette16 &targetPalette, uint8_t pal) {
// there is one randomly generated palette (1) followed by 4 palettes created from segment colors (2-5)
// those are followed by 7 fastled palettes (6-12) and 59 gradient palettes (13-71)
// then come the custom palettes (255,254,...) growing downwards from 255 (255 being 1st custom palette)
// then come user custom palettes (IDs <=200) and usermod palettes (IDs 201-255), both growing downward from their respective base IDs
// palette 0 is a varying palette depending on effect and may be replaced by segment's color if so
// instructed in color_from_palette()
if (pal > FIXED_PALETTE_COUNT && pal <= 255-customPalettes.size()) pal = 0; // out of bounds palette
//default palette. Differs depending on effect
if (pal == 0) pal = _default_palette; // _default_palette is set in setMode()
if (pal == 0) pal = _default_palette; // _default_palette is set in setMode(), differs depending on effect
const int umCount = usermodPalettes.size();
const int custCount = customPalettes.size();
if (pal >= FIXED_PALETTE_COUNT) {
if (pal > WLED_CUSTOM_PALETTE_ID_BASE) { // usermod range (IDs 201-255)
if ((WLED_USERMOD_PALETTE_ID_BASE - pal) >= umCount) pal = 0;
} else { // custom range
if ((WLED_CUSTOM_PALETTE_ID_BASE - pal) >= custCount) pal = 0;
}
}
switch (pal) {
case 0: //default palette. Exceptions for specific effects above
targetPalette = PartyColors_p;
targetPalette = PartyColors_gc22;
break;
case 1: //randomly generated palette
targetPalette = _randomPalette; //random palette is generated at intervals in handleRandomPalette()
@@ -266,21 +274,24 @@ CRGBPalette16 &Segment::loadPalette(CRGBPalette16 &targetPalette, uint8_t pal) {
}
break;}
default: //progmem palettes
if (pal > 255 - customPalettes.size()) {
targetPalette = customPalettes[255-pal]; // we checked bounds above
} else if (pal < DYNAMIC_PALETTE_COUNT+FASTLED_PALETTE_COUNT+1) { // palette 6 - 12, fastled palettes
targetPalette = *fastledPalettes[pal-DYNAMIC_PALETTE_COUNT-1];
if (pal > WLED_CUSTOM_PALETTE_ID_BASE) { // usermod palette
targetPalette = usermodPalettes[WLED_USERMOD_PALETTE_ID_BASE - pal].palette;
} else if (pal >= FIXED_PALETTE_COUNT) { // user custom palette
targetPalette = customPalettes[WLED_CUSTOM_PALETTE_ID_BASE - pal];
} else if (pal < DYNAMIC_PALETTE_COUNT + FASTLED_PALETTE_COUNT) { // palette 6 - 12, fastled palettes
targetPalette = *fastledPalettes[pal - DYNAMIC_PALETTE_COUNT];
} else {
byte tcp[72];
memcpy_P(tcp, (byte*)pgm_read_dword(&(gGradientPalettes[pal-(DYNAMIC_PALETTE_COUNT+FASTLED_PALETTE_COUNT)-1])), 72);
memcpy_P(tcp, (byte*)pgm_read_dword(&(gGradientPalettes[pal - (DYNAMIC_PALETTE_COUNT + FASTLED_PALETTE_COUNT)])), sizeof(tcp));
targetPalette.loadDynamicGradientPalette(tcp);
}
break;
}
return targetPalette;
}
// starting a transition has to occur before change so we get current values 1st
// note: _t is the temporary segment that holds the values transitioned from (palette, colors, brightness,...) and the current segment holds the "to" values
// if this is a non FADE transition or an FX change, the _oldSegment is created which is a full copy of the segment before the change
void Segment::startTransition(uint16_t dur, bool segmentCopy) {
if (dur == 0 || !isActive()) {
if (isInTransition()) _t->_dur = 0;
@@ -290,15 +301,42 @@ void Segment::startTransition(uint16_t dur, bool segmentCopy) {
if (segmentCopy && !_t->_oldSegment) {
// already in transition but segment copy requested and not yet created
_t->_oldSegment = new(std::nothrow) Segment(*this); // store/copy current segment settings
_t->_start = millis(); // restart countdown
_t->_start = millis(); // restart transition timer
_t->_dur = dur;
_t->_prevPaletteBlends = 0;
_t->_prevPaletteBlends = 0; // reset palette blends
if (_t->_oldSegment) {
_t->_oldSegment->palette = _t->_palette; // restore original palette and colors (from start of transition)
_t->_oldSegment->palette = _t->_palette; // restore original palette, colors, brightness and CCT (from start of transition)
for (unsigned i = 0; i < NUM_COLORS; i++) _t->_oldSegment->colors[i] = _t->_colors[i];
_t->_oldSegment->opacity = _t->_bri;
_t->_oldSegment->cct = _t->_cct;
// if already partway through a FADE transition, set old segment's colors to current blend to avoid jumping back to original colors
if (_t->_progress > 0) {
// already in a transition, see comment below
for (unsigned i = 0; i < NUM_COLORS; i++) _t->_oldSegment->colors[i] = color_blend16(_t->_colors[i], colors[i], _t->_progress);
_t->_oldSegment->opacity = currentBri(); // update "original" brightness note: _t->_progress is updated in updateTransitionProgress() so still valid here
_t->_oldSegment->cct = currentCCT(); // update "original" CCT (reduces jump)
}
DEBUGFX_PRINTF_P(PSTR("-- Updated transition with segment copy: S=%p T(%p) O[%p] OP[%p]\n"), this, _t, _t->_oldSegment, _t->_oldSegment->pixels);
if (!_t->_oldSegment->isActive()) stopTransition();
}
} else if (_t->_progress > 0) {
// already in a transition: capture the current visual blend as the new "from" state so the incoming change does not cause a visible jump.
// _palT already holds the intermediate blended palette and will continue blending toward the new target (see beginDraw()), so no palette action needed.
// initial version by @blazoncek (https://github.com/blazoncek/WLED/commit/40d9812)
for (unsigned i = 0; i < NUM_COLORS; i++) _t->_colors[i] = color_blend16(_t->_colors[i], colors[i], _t->_progress);
_t->_bri = currentBri(); // update "original" brightness note: _t->_progress is updated in updateTransitionProgress() so still valid here
_t->_cct = currentCCT(); // update "original" CCT (reduces jump)
// restart transition timer only if a pure FADE transition, otherwise let the FX change or non-FADE transition finish
// this avoids a re-start of the transition if color or brightness is changed during an ongoing FX or non-FADE transition
if (blendingStyle == TRANSITION_FADE) {
if (_t->_oldSegment != nullptr) {
if (_t->_oldSegment->mode != mode)
return; // do not reset transition if this is an FX change, note: the disadvantage is that colors still jump in that case
}
_t->_start = millis();
_t->_dur = dur;
_t->_prevPaletteBlends = 0;
}
}
return;
}
@@ -324,6 +362,7 @@ void Segment::startTransition(uint16_t dur, bool segmentCopy) {
}
void Segment::stopTransition() {
if (_t == nullptr) return; // no ongoing transition
DEBUG_PRINTF_P(PSTR("-- Stopping transition: S=%p T(%p) O[%p]\n"), this, _t, _t->_oldSegment);
delete _t;
_t = nullptr;
@@ -343,7 +382,7 @@ void Segment::updateTransitionProgress() const {
uint8_t Segment::currentCCT() const {
unsigned prog = progress();
if (prog < 0xFFFFU) {
if (blendingStyle == BLEND_STYLE_FADE) return (cct * prog + (_t->_cct * (0xFFFFU - prog))) / 0xFFFFU;
if (blendingStyle == TRANSITION_FADE) return (cct * prog + (_t->_cct * (0xFFFFU - prog))) / 0xFFFFU;
//else return Segment::isPreviousMode() ? _t->_cct : cct;
}
return cct;
@@ -355,8 +394,8 @@ uint8_t Segment::currentBri() const {
unsigned curBri = on ? opacity : 0;
if (prog < 0xFFFFU) {
// this will blend opacity in new mode if style is FADE (single effect call)
if (blendingStyle == BLEND_STYLE_FADE) curBri = (prog * curBri + _t->_bri * (0xFFFFU - prog)) / 0xFFFFU;
else curBri = Segment::isPreviousMode() ? _t->_bri : curBri;
if (blendingStyle == TRANSITION_FADE) curBri = (prog * curBri + _t->_bri * (0xFFFFU - prog)) / 0xFFFFU;
else curBri = Segment::isPreviousMode() ? _t->_bri : curBri;
}
return curBri;
}
@@ -371,7 +410,7 @@ void Segment::beginDraw(uint16_t prog) {
for (unsigned i = 0; i < NUM_COLORS; i++) _currentColors[i] = colors[i];
// load palette into _currentPalette
loadPalette(Segment::_currentPalette, palette);
if (isInTransition() && prog < 0xFFFFU && blendingStyle == BLEND_STYLE_FADE) {
if (isInTransition() && prog < 0xFFFFU && blendingStyle == TRANSITION_FADE) {
// blend colors
for (unsigned i = 0; i < NUM_COLORS; i++) _currentColors[i] = color_blend16(_t->_colors[i], colors[i], prog);
// blend palettes
@@ -379,7 +418,7 @@ void Segment::beginDraw(uint16_t prog) {
// minimum blend time is 100ms maximum is 65535ms
#ifndef WLED_SAVE_RAM
unsigned noOfBlends = ((255U * prog) / 0xFFFFU) - _t->_prevPaletteBlends;
if(noOfBlends > 255) noOfBlends = 255; // safety check
if (noOfBlends > 255) noOfBlends = 255; // safety check
for (unsigned i = 0; i < noOfBlends; i++, _t->_prevPaletteBlends++) nblendPaletteTowardPalette(_t->_palT, Segment::_currentPalette, 48);
Segment::_currentPalette = _t->_palT; // copy transitioning/temporary palette
#else
@@ -443,7 +482,7 @@ void Segment::setGeometry(uint16_t i1, uint16_t i2, uint8_t grp, uint8_t spc, ui
DEBUGFX_PRINTF_P(PSTR("Segment geometry: %d,%d -> %d,%d [%d,%d]\n"), (int)i1, (int)i2, (int)i1Y, (int)i2Y, (int)grp, (int)spc);
markForReset();
if (_t) stopTransition(); // we can't use transition if segment dimensions changed
stopTransition(); // we can't use transition if segment dimensions changed
stateChanged = true; // send UDP/WS broadcast
// apply change immediately
@@ -458,7 +497,7 @@ void Segment::setGeometry(uint16_t i1, uint16_t i2, uint8_t grp, uint8_t spc, ui
return;
}
if (i1 < Segment::maxWidth || (i1 >= Segment::maxWidth*Segment::maxHeight && i1 < strip.getLengthTotal())) start = i1; // Segment::maxWidth equals strip.getLengthTotal() for 1D
stop = i2 > Segment::maxWidth*Segment::maxHeight ? MIN(i2,strip.getLengthTotal()) : constrain(i2, 1, Segment::maxWidth);
stop = i2 > Segment::maxWidth*Segment::maxHeight && i1 >= Segment::maxWidth*Segment::maxHeight ? MIN(i2,strip.getLengthTotal()) : constrain(i2, 1, Segment::maxWidth); // check for 2D trailing strip
startY = 0;
stopY = 1;
#ifndef WLED_DISABLE_2D
@@ -506,7 +545,7 @@ Segment &Segment::setColor(uint8_t slot, uint32_t c) {
if (slot == 1 && c != BLACK) return *this; // on/off segment cannot have secondary color non black
}
//DEBUG_PRINTF_P(PSTR("- Starting color transition: %d [0x%X]\n"), slot, c);
startTransition(strip.getTransition(), blendingStyle != BLEND_STYLE_FADE); // start transition prior to change
startTransition(strip.getTransition(), blendingStyle != TRANSITION_FADE); // start transition prior to change
colors[slot] = c;
stateChanged = true; // send UDP/WS broadcast
return *this;
@@ -530,7 +569,7 @@ Segment &Segment::setCCT(uint16_t k) {
Segment &Segment::setOpacity(uint8_t o) {
if (opacity != o) {
//DEBUG_PRINTF_P(PSTR("- Starting opacity transition: %d\n"), o);
startTransition(strip.getTransition(), blendingStyle != BLEND_STYLE_FADE); // start transition prior to change
startTransition(strip.getTransition(), blendingStyle != TRANSITION_FADE); // start transition prior to change
opacity = o;
stateChanged = true; // send UDP/WS broadcast
}
@@ -541,7 +580,7 @@ Segment &Segment::setOption(uint8_t n, bool val) {
bool prev = (options >> n) & 0x01;
if (val == prev) return *this;
//DEBUG_PRINTF_P(PSTR("- Starting option transition: %d\n"), n);
if (n == SEG_OPTION_ON) startTransition(strip.getTransition(), blendingStyle != BLEND_STYLE_FADE); // start transition prior to change
if (n == SEG_OPTION_ON) startTransition(strip.getTransition(), blendingStyle != TRANSITION_FADE); // start transition prior to change
if (val) options |= 0x01 << n;
else options &= ~(0x01 << n);
stateChanged = true; // send UDP/WS broadcast
@@ -577,7 +616,7 @@ Segment &Segment::setMode(uint8_t fx, bool loadDefaults) {
sOpt = extractModeDefaults(fx, "pal"); // always extract 'pal' to set _default_palette
if (sOpt >= 0 && loadDefaults) setPalette(sOpt);
if (sOpt <= 0) sOpt = 6; // partycolors if zero or not set
_default_palette = sOpt; // _deault_palette is loaded into pal0 in loadPalette() (if selected)
_default_palette = sOpt; // _default_palette is loaded into pal0 in loadPalette() (if selected)
markForReset();
stateChanged = true; // send UDP/WS broadcast
}
@@ -585,10 +624,16 @@ Segment &Segment::setMode(uint8_t fx, bool loadDefaults) {
}
Segment &Segment::setPalette(uint8_t pal) {
if (pal <= 255-customPalettes.size() && pal > FIXED_PALETTE_COUNT) pal = 0; // not built in palette or custom palette
if (pal >= FIXED_PALETTE_COUNT) {
if (pal > WLED_CUSTOM_PALETTE_ID_BASE) { // usermod range
if ((WLED_USERMOD_PALETTE_ID_BASE - pal) >= (int)usermodPalettes.size()) pal = 0;
} else { // custom range
if ((WLED_CUSTOM_PALETTE_ID_BASE - pal) >= (int)customPalettes.size()) pal = 0;
}
}
if (pal != palette) {
//DEBUG_PRINTF_P(PSTR("- Starting palette transition: %d\n"), pal);
startTransition(strip.getTransition(), blendingStyle != BLEND_STYLE_FADE); // start transition prior to change (no need to copy segment)
startTransition(strip.getTransition(), blendingStyle != TRANSITION_FADE); // start transition prior to change (no need to copy segment)
palette = pal;
stateChanged = true; // send UDP/WS broadcast
}
@@ -692,11 +737,11 @@ uint16_t Segment::maxMappingLength() const {
// pixel is clipped if it falls outside clipping range
// if clipping start > stop the clipping range is inverted
bool Segment::isPixelClipped(int i) const {
if (blendingStyle != BLEND_STYLE_FADE && isInTransition() && _clipStart != _clipStop) {
if (blendingStyle != TRANSITION_FADE && isInTransition() && _clipStart != _clipStop) {
bool invert = _clipStart > _clipStop; // ineverted start & stop
int start = invert ? _clipStop : _clipStart;
int stop = invert ? _clipStart : _clipStop;
if (blendingStyle == BLEND_STYLE_FAIRY_DUST) {
if (blendingStyle == TRANSITION_FAIRY_DUST) {
unsigned len = stop - start;
if (len < 2) return false;
unsigned shuffled = hashInt(i) % len;
@@ -1102,11 +1147,11 @@ void Segment::blur(uint8_t blur_amount, bool smear) const {
* Rotates the color in HSV space, where pos is H. (0=0deg, 256=360deg)
*/
uint32_t Segment::color_wheel(uint8_t pos) const {
if (palette) return color_from_palette(pos, false, false, 0); // only wrap if "always wrap" is set
if (palette) return color_from_palette(pos, false, true, 0); // color_wheel is a continuous (moving) wheel, so wrap end->start (restores pre-0.16 behaviour)
uint8_t w = W(getCurrentColor(0));
uint32_t rgb;
hsv2rgb(CHSV32(static_cast<uint16_t>(pos << 8), 255, 255), rgb);
return rgb | (w << 24); // add white channel
CRGBW rgb;
rgb = CHSV32(static_cast<uint16_t>(pos << 8), 255, 255);
return rgb.color32 | (w << 24); // add white channel
}
/*
@@ -1159,66 +1204,74 @@ void WS2812FX::finalizeInit() {
_hasWhiteChannel = _isOffRefreshRequired = false;
BusManager::removeAll();
// TODO: ideally we would free everything segment related here to reduce fragmentation (pixel buffers, ledamp, segments, etc) but that somehow leads to heap corruption if touchig any of the buffers.
unsigned digitalCount = 0;
#if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32C3)
// determine if it is sensible to use parallel I2S outputs on ESP32 (i.e. more than 5 outputs = 1 I2S + 4 RMT)
unsigned maxLedsOnBus = 0;
unsigned busType = 0;
// validate the bus config: count I2S buses and check if they meet requirements
unsigned i2sBusCount = 0;
for (const auto &bus : busConfigs) {
if (Bus::isDigital(bus.type) && !Bus::is2Pin(bus.type)) {
digitalCount++;
if (busType == 0) busType = bus.type; // remember first bus type
if (busType != bus.type) {
DEBUG_PRINTF_P(PSTR("Mixed digital bus types detected! Forcing single I2S output.\n"));
useParallelI2S = false; // mixed bus types, no parallel I2S
}
if (bus.count > maxLedsOnBus) maxLedsOnBus = bus.count;
if (bus.driverType == 1)
i2sBusCount++;
}
}
DEBUG_PRINTF_P(PSTR("Maximum LEDs on a bus: %u\nDigital buses: %u\n"), maxLedsOnBus, digitalCount);
// we may remove 600 LEDs per bus limit when NeoPixelBus is updated beyond 2.8.3
if (maxLedsOnBus <= 600 && useParallelI2S) BusManager::useParallelOutput(); // must call before creating buses
else useParallelI2S = false; // enforce single I2S
digitalCount = 0;
DEBUG_PRINTF_P(PSTR("Digital buses: %u, I2S buses: %u\n"), digitalCount, i2sBusCount);
// Determine parallel vs single I2S usage (used for memory calculation only)
bool useParallelI2S = false;
#if defined(CONFIG_IDF_TARGET_ESP32S3)
// ESP32-S3 always uses parallel LCD driver for I2S
if (i2sBusCount > 0) {
useParallelI2S = true;
}
#else
if (i2sBusCount > 1) {
useParallelI2S = true;
}
#endif
#endif
DEBUG_PRINTF_P(PSTR("Heap before buses: %d\n"), getFreeHeapSize());
// create buses/outputs
unsigned mem = 0;
unsigned maxI2S = 0;
for (const auto &bus : busConfigs) {
unsigned memB = bus.memUsage(Bus::isDigital(bus.type) && !Bus::is2Pin(bus.type) ? digitalCount++ : 0); // does not include DMA/RMT buffer
mem += memB;
// estimate maximum I2S memory usage (only relevant for digital non-2pin busses)
unsigned mem = 0; // memory estimation including DMA buffer for I2S and pixel buffers
unsigned I2SdmaMem = 0;
for (auto &bus : busConfigs) {
// assign bus types: call to getI() determines bus types/drivers, allocates and tracks polybus channels
// store the result in iType for later use during bus creation (getI() must only be called once per BusConfig)
// note: this needs to be determined for all buses prior to creating them as it also determines parallel I2S usage
bus.iType = BusManager::getI(bus.type, bus.pins, bus.driverType);
}
for (auto &bus : busConfigs) {
bool use_placeholder = false;
unsigned busMemUsage = bus.memUsage(); // does not include DMA/RMT buffer but includes pixel buffers (segment buffer + global buffer)
mem += busMemUsage;
// estimate maximum I2S memory usage (only relevant for digital non-2pin busses when I2S is enabled)
#if !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(ESP8266)
#if defined(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32S3)
const bool usesI2S = ((useParallelI2S && digitalCount <= 8) || (!useParallelI2S && digitalCount == 1));
#elif defined(CONFIG_IDF_TARGET_ESP32S2)
const bool usesI2S = (useParallelI2S && digitalCount <= 8);
#else
const bool usesI2S = false;
#endif
bool usesI2S = (bus.iType & 0x01) == 0; // I2S bus types are even numbered, can't use bus.driverType == 1 as getI() may have defaulted to RMT
if (Bus::isDigital(bus.type) && !Bus::is2Pin(bus.type) && usesI2S) {
#ifdef NPB_CONF_4STEP_CADENCE
constexpr unsigned stepFactor = 4; // 4 step cadence (4 bits per pixel bit)
#else
constexpr unsigned stepFactor = 3; // 3 step cadence (3 bits per pixel bit)
#endif
unsigned i2sCommonSize = stepFactor * bus.count * (3*Bus::hasRGB(bus.type)+Bus::hasWhite(bus.type)+Bus::hasCCT(bus.type)) * (Bus::is16bit(bus.type)+1);
if (i2sCommonSize > maxI2S) maxI2S = i2sCommonSize;
unsigned i2sCommonMem = (stepFactor * bus.count * (3*Bus::hasRGB(bus.type)+Bus::hasWhite(bus.type)+Bus::hasCCT(bus.type)) * (Bus::is16bit(bus.type)+1));
if (useParallelI2S) i2sCommonMem *= 8; // parallel I2S uses 8 channels, requiring 8x the DMA buffer size (common buffer shared between all parallel busses)
if (i2sCommonMem > I2SdmaMem) I2SdmaMem = i2sCommonMem;
}
#endif
if (mem + maxI2S <= MAX_LED_MEMORY) {
BusManager::add(bus);
DEBUG_PRINTF_P(PSTR("Bus memory: %uB\n"), memB);
} else {
errorFlag = ERR_NORAM_PX; // alert UI
DEBUG_PRINTF_P(PSTR("Out of LED memory! Bus %d (%d) #%u not created."), (int)bus.type, (int)bus.count, digitalCount);
break;
if (mem + I2SdmaMem > MAX_LED_MEMORY + 1024) { // +1k to allow some margin to not drop buses that are allowed in UI (calculation here includes bus overhead)
DEBUG_PRINTF_P(PSTR("Bus %d with %d LEDS memory usage exceeds limit\n"), (int)bus.type, bus.count);
errorFlag = ERR_NORAM; // alert UI TODO: make this a distinct error: not enough memory for bus
use_placeholder = true;
}
if (BusManager::add(bus, use_placeholder) != -1) {
mem += BusManager::busses.back()->getBusSize();
if (Bus::isDigital(bus.type) && !Bus::is2Pin(bus.type) && BusManager::busses.back()->isPlaceholder()) digitalCount--; // remove placeholder from digital count
}
}
DEBUG_PRINTF_P(PSTR("LED buffer size: %uB/%uB\n"), mem + maxI2S, BusManager::memUsage());
DEBUG_PRINTF_P(PSTR("Estimated buses + pixel-buffers size: %uB\n"), mem + I2SdmaMem);
busConfigs.clear();
busConfigs.shrink_to_fit();
@@ -1249,71 +1302,71 @@ void WS2812FX::finalizeInit() {
deserializeMap(); // (re)load default ledmap (will also setUpMatrix() if ledmap does not exist)
// allocate frame buffer after matrix has been set up (gaps!)
updatePixelBuffer();
DEBUG_PRINTF_P(PSTR("Heap after strip init: %uB\n"), getFreeHeapSize());
}
// update global _pixels[] buffer to match getLengthTotal() note: if allocation fails, WLED will not render anything
void WS2812FX::updatePixelBuffer() {
uint32_t requiredMem = getLengthTotal() * sizeof(uint32_t);
p_free(_pixels); // using realloc on large buffers can cause additional fragmentation instead of reducing it
// use PSRAM if available: there is no measurable perfomance impact between PSRAM and DRAM on S2/S3 with QSPI PSRAM for this buffer
_pixels = static_cast<uint32_t*>(allocate_buffer(getLengthTotal() * sizeof(uint32_t), BFRALLOC_ENFORCE_PSRAM | BFRALLOC_NOBYTEACCESS | BFRALLOC_CLEAR));
DEBUG_PRINTF_P(PSTR("strip buffer size: %uB\n"), getLengthTotal() * sizeof(uint32_t));
DEBUG_PRINTF_P(PSTR("Heap after strip init: %uB\n"), getFreeHeapSize());
_pixels = static_cast<uint32_t*>(allocate_buffer(requiredMem, BFRALLOC_ENFORCE_PSRAM | BFRALLOC_NOBYTEACCESS | BFRALLOC_CLEAR));
DEBUG_PRINTF_P(PSTR("strip buffer size: %uB\n"), requiredMem);
}
void WS2812FX::service() {
unsigned long nowUp = millis(); // Be aware, millis() rolls over every 49 days
now = nowUp + timebase;
unsigned long elapsed = nowUp - _lastServiceShow;
if (_suspend || elapsed <= MIN_FRAME_DELAY) return; // keep wifi alive - no matter if triggered or unlimited
if (!_triggered && (_targetFps != FPS_UNLIMITED)) { // unlimited mode = no frametime
if (elapsed < _frametime) return; // too early for service
}
bool timeToShow = (elapsed >= _frametime); // all segments are running at the same speed
if (_triggered || _targetFps == FPS_UNLIMITED) timeToShow = true; // unlimited mode = no frametime; strip.trigger() can overrule timing
bool doShow = false;
now = nowUp + timebase; // common time base for all effects
if (!timeToShow) return; // too early for service
if (_suspend || elapsed <= MIN_FRAME_DELAY) return; // keep wifi alive - no matter if triggered or unlimited
_isServicing = true;
_segment_index = 0;
for (Segment &seg : _segments) {
if (_suspend) break; // immediately stop processing segments if suspend requested during service()
bool doShow = _triggered; // true if ≥1 active segment was processed (and strip was not suspended mid-loop), or trigger received → triggers show()
for (size_t i = 0; i < _segments.size(); i++) {
Segment &seg = _segments[i];
_segment_index = i;
if (_suspend) break; // abort processing segments if suspend requested during service()
// process transition (also pre-calculates progress value)
seg.handleTransition();
// reset the segment runtime data if needed
seg.resetIfRequired();
if (!seg.isActive()) continue;
// last condition ensures all solid segments are updated at the same time
if (nowUp > seg.next_time || _triggered || (doShow && seg.mode == FX_MODE_STATIC))
{
if (seg.isActive()) {
// current segment is active -> re-run effect, and remember that show() call is necessary
// if we arrive here, its always showtime (timeToShow == true)
doShow = true;
unsigned frameDelay = FRAMETIME;
if (!seg.freeze) { //only run effect function if not frozen
// Effect blending
uint16_t prog = seg.progress();
seg.beginDraw(prog); // set up parameters for get/setPixelColor() (will also blend colors and palette if blend style is FADE)
_currentSegment = &seg; // set current segment for effect functions (SEGMENT & SEGENV)
// workaround for on/off transition to respect blending style
frameDelay = (*_mode[seg.mode])(); // run new/current mode (needed for bri workaround)
_mode[seg.mode](); // run new/current mode (needed for bri workaround)
seg.call++;
// if segment is in transition and no old segment exists we don't need to run the old mode
// (blendSegments() takes care of On/Off transitions and clipping)
Segment *segO = seg.getOldSegment();
if (segO && segO->isActive() && (seg.mode != segO->mode || blendingStyle != BLEND_STYLE_FADE ||
if (segO && segO->isActive() && (seg.mode != segO->mode || blendingStyle != TRANSITION_FADE ||
(segO->name != seg.name && segO->name && seg.name && strncmp(segO->name, seg.name, WLED_MAX_SEGNAME_LEN) != 0))) {
Segment::modeBlend(true); // set semaphore for beginDraw() to blend colors and palette
Segment::modeBlend(true); // set flag for beginDraw() to blend colors and palette
segO->beginDraw(prog); // set up palette & colors (also sets draw dimensions), parent segment has transition progress
_currentSegment = segO; // set current segment
// workaround for on/off transition to respect blending style
frameDelay = min(frameDelay, (unsigned)(*_mode[segO->mode])()); // run old mode (needed for bri workaround; semaphore!!)
_mode[segO->mode](); // run old mode (needed for bri workaround; semaphore!!)
segO->call++; // increment old mode run counter
Segment::modeBlend(false); // unset semaphore
Segment::modeBlend(false); // unset flag
}
if (seg.isInTransition() && frameDelay > FRAMETIME) frameDelay = FRAMETIME; // force faster updates during transition
}
seg.next_time = nowUp + frameDelay;
}
_segment_index++;
}
_segment_index = 0; // segment index is only valid while effects are serviced
_currentSegment = &_segments[0]; // safe fallback to prevent stale pointer - SEGMENT/SEGENV should not be used outside of the service loop
#ifdef WLED_DEBUG
if ((_targetFps != FPS_UNLIMITED) && (millis() - nowUp > _frametime)) DEBUG_PRINTF_P(PSTR("Slow effects %u/%d.\n"), (unsigned)(millis()-nowUp), (int)_frametime);
@@ -1328,14 +1381,14 @@ void WS2812FX::service() {
if ((_targetFps != FPS_UNLIMITED) && (millis() - nowUp > _frametime)) DEBUG_PRINTF_P(PSTR("Slow strip %u/%d.\n"), (unsigned)(millis()-nowUp), (int)_frametime);
#endif
_triggered = false;
if (!_suspend) _triggered = false; // avoid losing "trigger" events if suspend requested during effect service()
_isServicing = false;
}
// https://en.wikipedia.org/wiki/Blend_modes but using a for top layer & b for bottom layer
static uint8_t _top (uint8_t a, uint8_t b) { return a; }
static uint8_t _bottom (uint8_t a, uint8_t b) { return b; }
static uint8_t _add (uint8_t a, uint8_t b) { unsigned t = a + b; return t > 255 ? 255 : t; }
static uint8_t _top (uint8_t a, uint8_t b) { return a; } // function unused
static uint8_t _bottom (uint8_t a, uint8_t b) { return b; } // function unused
static uint8_t _add (uint8_t a, uint8_t b) { unsigned t = a + b; return t > 255 ? 255 : t; } // function unused
static uint8_t _subtract (uint8_t a, uint8_t b) { return b > a ? (b - a) : 0; }
static uint8_t _difference(uint8_t a, uint8_t b) { return b > a ? (b - a) : (a - b); }
static uint8_t _average (uint8_t a, uint8_t b) { return (a + b) >> 1; }
@@ -1357,24 +1410,41 @@ static uint8_t _softlight (uint8_t a, uint8_t b) { return (b * b * (255 - 2 * a)
#endif
static uint8_t _dodge (uint8_t a, uint8_t b) { return _divide(~a,b); }
static uint8_t _burn (uint8_t a, uint8_t b) { return ~_divide(a,~b); }
static uint8_t _stencil (uint8_t a, uint8_t b) { return a ? a : b; } // function unused
static uint8_t _dummy (uint8_t a, uint8_t b) { return a; } // dummy (same as _top) to fill the function list and make it safe from OOB access
#define BLENDMODES 17 // number of blend modes must match "bm" in index.js, all cases must be handled in segblend() @ blendSegment()
void WS2812FX::blendSegment(const Segment &topSegment) const {
typedef uint8_t(*FuncType)(uint8_t, uint8_t);
// function pointer array: fill with _dummy if using special case: avoid OOB access and always provide a valid path
FuncType funcs[] = {
_top, _bottom,
_add, _subtract, _difference, _average,
_multiply, _divide, _lighten, _darken, _screen, _overlay,
_hardlight, _softlight, _dodge, _burn
_dummy, _dummy, _dummy, _subtract,
_difference, _average, _dummy, _divide,
_lighten, _darken, _screen, _overlay,
_hardlight, _softlight, _dodge, _burn,
_dummy
};
const size_t blendMode = topSegment.blendMode < (sizeof(funcs) / sizeof(FuncType)) ? topSegment.blendMode : 0;
const auto func = funcs[blendMode]; // blendMode % (sizeof(funcs) / sizeof(FuncType))
const auto blend = [&](uint32_t top, uint32_t bottom){ return RGBW32(func(R(top),R(bottom)), func(G(top),G(bottom)), func(B(top),B(bottom)), func(W(top),W(bottom))); };
const size_t blendMode = topSegment.blendMode < BLENDMODES ? topSegment.blendMode : 0; // default to top if unsupported mode
const auto segblend = [&](uint32_t t, uint32_t b){
// use direct calculations/returns for simple/frequent modes (faster)
switch (blendMode) {
case 0 : return t; // top
case 1 : return b; // bottom
case 2 : return color_add(t,b,true); // add with preserve color ratio to avoid color clipping
case 6 : return RGBW32(_multiply(R(t),R(b)), _multiply(G(t),G(b)), _multiply(B(t),B(b)), _multiply(W(t),W(b))); // multiply (7% faster than lambda at 100bytes flash cost)
case 16: return t ? t : b; // stencil (use top layer if not black, else bottom)
}
// default: use function pointer from array
const auto func = funcs[blendMode];
return RGBW32(func(R(t),R(b)), func(G(t),G(b)), func(B(t),B(b)), func(W(t),W(b)));
};
const int length = topSegment.length(); // physical segment length (counts all pixels in 2D segment)
const int width = topSegment.width();
const int height = topSegment.height();
//const uint32_t bgColor = topSegment.colors[1]; // background color (unused, could add it to stencil mode if requested)
const auto XY = [](int x, int y){ return x + y*Segment::maxWidth; };
const size_t matrixSize = Segment::maxWidth * Segment::maxHeight;
const size_t startIndx = XY(topSegment.start, topSegment.startY);
@@ -1383,61 +1453,62 @@ void WS2812FX::blendSegment(const Segment &topSegment) const {
const unsigned progInv = 0xFFFFU - progress;
uint8_t opacity = topSegment.currentBri(); // returns transitioned opacity for style FADE
uint8_t cct = topSegment.currentCCT();
if (gammaCorrectCol) opacity = gamma8inv(opacity); // use inverse gamma on brightness for correct color scaling after gamma correction (see #5343 for details)
Segment::setClippingRect(0, 0); // disable clipping by default
const unsigned dw = (blendingStyle==BLEND_STYLE_OUTSIDE_IN ? progInv : progress) * width / 0xFFFFU + 1;
const unsigned dh = (blendingStyle==BLEND_STYLE_OUTSIDE_IN ? progInv : progress) * height / 0xFFFFU + 1;
const unsigned dw = (blendingStyle==TRANSITION_OUTSIDE_IN ? progInv : progress) * width / 0xFFFFU + 1;
const unsigned dh = (blendingStyle==TRANSITION_OUTSIDE_IN ? progInv : progress) * height / 0xFFFFU + 1;
const unsigned orgBS = blendingStyle;
if (width*height == 1) blendingStyle = BLEND_STYLE_FADE; // disable style for single pixel segments (use fade instead)
if (width*height == 1) blendingStyle = TRANSITION_FADE; // disable style for single pixel segments (use fade instead)
switch (blendingStyle) {
case BLEND_STYLE_CIRCULAR_IN: // (must set entire segment, see isPixelXYClipped())
case BLEND_STYLE_CIRCULAR_OUT:// (must set entire segment, see isPixelXYClipped())
case BLEND_STYLE_FAIRY_DUST: // fairy dust (must set entire segment, see isPixelXYClipped())
case TRANSITION_CIRCULAR_IN: // (must set entire segment, see isPixelXYClipped())
case TRANSITION_CIRCULAR_OUT:// (must set entire segment, see isPixelXYClipped())
case TRANSITION_FAIRY_DUST: // fairy dust (must set entire segment, see isPixelXYClipped())
Segment::setClippingRect(0, width, 0, height);
break;
case BLEND_STYLE_SWIPE_RIGHT: // left-to-right
case BLEND_STYLE_PUSH_RIGHT: // left-to-right
case TRANSITION_SWIPE_RIGHT: // left-to-right
case TRANSITION_PUSH_RIGHT: // left-to-right
Segment::setClippingRect(0, dw, 0, height);
break;
case BLEND_STYLE_SWIPE_LEFT: // right-to-left
case BLEND_STYLE_PUSH_LEFT: // right-to-left
case TRANSITION_SWIPE_LEFT: // right-to-left
case TRANSITION_PUSH_LEFT: // right-to-left
Segment::setClippingRect(width - dw, width, 0, height);
break;
case BLEND_STYLE_OUTSIDE_IN: // corners
case TRANSITION_OUTSIDE_IN: // corners
Segment::setClippingRect((width + dw)/2, (width - dw)/2, (height + dh)/2, (height - dh)/2); // inverted!!
break;
case BLEND_STYLE_INSIDE_OUT: // outward
case TRANSITION_INSIDE_OUT: // outward
Segment::setClippingRect((width - dw)/2, (width + dw)/2, (height - dh)/2, (height + dh)/2);
break;
case BLEND_STYLE_SWIPE_DOWN: // top-to-bottom (2D)
case BLEND_STYLE_PUSH_DOWN: // top-to-bottom (2D)
case TRANSITION_SWIPE_DOWN: // top-to-bottom (2D)
case TRANSITION_PUSH_DOWN: // top-to-bottom (2D)
Segment::setClippingRect(0, width, 0, dh);
break;
case BLEND_STYLE_SWIPE_UP: // bottom-to-top (2D)
case BLEND_STYLE_PUSH_UP: // bottom-to-top (2D)
case TRANSITION_SWIPE_UP: // bottom-to-top (2D)
case TRANSITION_PUSH_UP: // bottom-to-top (2D)
Segment::setClippingRect(0, width, height - dh, height);
break;
case BLEND_STYLE_OPEN_H: // horizontal-outward (2D) same look as INSIDE_OUT on 1D
case TRANSITION_OPEN_H: // horizontal-outward (2D) same look as INSIDE_OUT on 1D
Segment::setClippingRect((width - dw)/2, (width + dw)/2, 0, height);
break;
case BLEND_STYLE_OPEN_V: // vertical-outward (2D)
case TRANSITION_OPEN_V: // vertical-outward (2D)
Segment::setClippingRect(0, width, (height - dh)/2, (height + dh)/2);
break;
case BLEND_STYLE_SWIPE_TL: // TL-to-BR (2D)
case BLEND_STYLE_PUSH_TL: // TL-to-BR (2D)
case TRANSITION_SWIPE_TL: // TL-to-BR (2D)
case TRANSITION_PUSH_TL: // TL-to-BR (2D)
Segment::setClippingRect(0, dw, 0, dh);
break;
case BLEND_STYLE_SWIPE_TR: // TR-to-BL (2D)
case BLEND_STYLE_PUSH_TR: // TR-to-BL (2D)
case TRANSITION_SWIPE_TR: // TR-to-BL (2D)
case TRANSITION_PUSH_TR: // TR-to-BL (2D)
Segment::setClippingRect(width - dw, width, 0, dh);
break;
case BLEND_STYLE_SWIPE_BR: // BR-to-TL (2D)
case BLEND_STYLE_PUSH_BR: // BR-to-TL (2D)
case TRANSITION_SWIPE_BR: // BR-to-TL (2D)
case TRANSITION_PUSH_BR: // BR-to-TL (2D)
Segment::setClippingRect(width - dw, width, height - dh, height);
break;
case BLEND_STYLE_SWIPE_BL: // BL-to-TR (2D)
case BLEND_STYLE_PUSH_BL: // BL-to-TR (2D)
case TRANSITION_SWIPE_BL: // BL-to-TR (2D)
case TRANSITION_PUSH_BL: // BL-to-TR (2D)
Segment::setClippingRect(0, dw, height - dh, height);
break;
}
@@ -1454,18 +1525,18 @@ void WS2812FX::blendSegment(const Segment &topSegment) const {
const int baseX = topSegment.start + x;
const int baseY = topSegment.startY + y;
size_t indx = XY(baseX, baseY); // absolute address on strip
_pixels[indx] = color_blend(_pixels[indx], blend(c, _pixels[indx]), o);
_pixels[indx] = color_blend(_pixels[indx], segblend(c, _pixels[indx]), o);
if (_pixelCCT) _pixelCCT[indx] = cct;
// Apply mirroring
// Apply mirroring if enabled
if (topSegment.mirror || topSegment.mirror_y) {
const int mirrorX = topSegment.start + width - x - 1;
const int mirrorY = topSegment.startY + height - y - 1;
const size_t idxMX = XY(topSegment.transpose ? baseX : mirrorX, topSegment.transpose ? mirrorY : baseY);
const size_t idxMY = XY(topSegment.transpose ? mirrorX : baseX, topSegment.transpose ? baseY : mirrorY);
const size_t idxMM = XY(mirrorX, mirrorY);
if (topSegment.mirror) _pixels[idxMX] = color_blend(_pixels[idxMX], blend(c, _pixels[idxMX]), o);
if (topSegment.mirror_y) _pixels[idxMY] = color_blend(_pixels[idxMY], blend(c, _pixels[idxMY]), o);
if (topSegment.mirror && topSegment.mirror_y) _pixels[idxMM] = color_blend(_pixels[idxMM], blend(c, _pixels[idxMM]), o);
if (topSegment.mirror) _pixels[idxMX] = color_blend(_pixels[idxMX], segblend(c, _pixels[idxMX]), o);
if (topSegment.mirror_y) _pixels[idxMY] = color_blend(_pixels[idxMY], segblend(c, _pixels[idxMY]), o);
if (topSegment.mirror && topSegment.mirror_y) _pixels[idxMM] = color_blend(_pixels[idxMM], segblend(c, _pixels[idxMM]), o);
if (_pixelCCT) {
if (topSegment.mirror) _pixelCCT[idxMX] = cct;
if (topSegment.mirror_y) _pixelCCT[idxMY] = cct;
@@ -1475,9 +1546,22 @@ void WS2812FX::blendSegment(const Segment &topSegment) const {
};
// if we blend using "push" style we need to "shift" canvas to left/right/up/down
unsigned offsetX = (blendingStyle == BLEND_STYLE_PUSH_UP || blendingStyle == BLEND_STYLE_PUSH_DOWN) ? 0 : progInv * nCols / 0xFFFFU;
unsigned offsetY = (blendingStyle == BLEND_STYLE_PUSH_LEFT || blendingStyle == BLEND_STYLE_PUSH_RIGHT) ? 0 : progInv * nRows / 0xFFFFU;
unsigned offsetX = (blendingStyle == TRANSITION_PUSH_UP || blendingStyle == TRANSITION_PUSH_DOWN) ? 0 : progInv * nCols / 0xFFFFU;
unsigned offsetY = (blendingStyle == TRANSITION_PUSH_LEFT || blendingStyle == TRANSITION_PUSH_RIGHT) ? 0 : progInv * nRows / 0xFFFFU;
const unsigned groupLen = topSegment.groupLength();
bool applyReverse = topSegment.reverse || topSegment.reverse_y || topSegment.transpose;
int pushOffsetX = 0, pushOffsetY = 0;
// if we blend using "push" style we need to "shift" canvas to left/right/up/down
switch (blendingStyle) {
case TRANSITION_PUSH_RIGHT: pushOffsetX = offsetX; break;
case TRANSITION_PUSH_LEFT: pushOffsetX = -offsetX + nCols; break;
case TRANSITION_PUSH_DOWN: pushOffsetY = offsetY; break;
case TRANSITION_PUSH_UP: pushOffsetY = -offsetY + nRows; break;
case TRANSITION_PUSH_TL: pushOffsetX = offsetX; pushOffsetY = offsetY; break; // unused
case TRANSITION_PUSH_TR: pushOffsetX = -offsetX + nCols; pushOffsetY = offsetY; break; // unused
case TRANSITION_PUSH_BR: pushOffsetX = -offsetX + nCols; pushOffsetY = -offsetY + nRows; break; // unused
case TRANSITION_PUSH_BL: pushOffsetX = offsetX; pushOffsetY = -offsetY + nRows; break; // unused
}
// we only traverse new segment, not old one
for (int r = 0; r < nRows; r++) for (int c = 0; c < nCols; c++) {
const bool clipped = topSegment.isPixelXYClipped(c, r);
@@ -1487,34 +1571,31 @@ void WS2812FX::blendSegment(const Segment &topSegment) const {
int vRows = seg == segO ? oRows : nRows; // old segment may have different dimensions
int x = c;
int y = r;
// if we blend using "push" style we need to "shift" canvas to left/right/up/down
switch (blendingStyle) {
case BLEND_STYLE_PUSH_RIGHT: x = (x + offsetX) % nCols; break;
case BLEND_STYLE_PUSH_LEFT: x = (x - offsetX + nCols) % nCols; break;
case BLEND_STYLE_PUSH_DOWN: y = (y + offsetY) % nRows; break;
case BLEND_STYLE_PUSH_UP: y = (y - offsetY + nRows) % nRows; break;
}
if (pushOffsetX != 0) x = (x + pushOffsetX) % nCols;
if (pushOffsetY != 0) y = (y + pushOffsetY) % nRows;
uint32_t c_a = BLACK;
if (x < vCols && y < vRows) c_a = seg->getPixelColorRaw(x + y*vCols); // will get clipped pixel from old segment or unclipped pixel from new segment
if (segO && blendingStyle == BLEND_STYLE_FADE
if (segO && blendingStyle == TRANSITION_FADE
&& (topSegment.mode != segO->mode || (segO->name != topSegment.name && segO->name && topSegment.name && strncmp(segO->name, topSegment.name, WLED_MAX_SEGNAME_LEN) != 0))
&& x < oCols && y < oRows) {
// we need to blend old segment using fade as pixels are not clipped
c_a = color_blend16(c_a, segO->getPixelColorRaw(x + y*oCols), progInv);
} else if (blendingStyle != BLEND_STYLE_FADE) {
} else if (blendingStyle != TRANSITION_FADE) {
// if we have global brightness change (not On/Off change) we will ignore transition style and just fade brightness (see led.cpp)
// workaround for On/Off transition
// (bri != briT) && !bri => from On to Off
// (bri != briT) && bri => from Off to On
if ((!clipped && (bri != briT) && !bri) || (clipped && (bri != briT) && bri)) c_a = BLACK;
if ((briOld == 0 || bri == 0) && ((!clipped && (bri != briT) && !bri) || (clipped && (bri != briT) && bri))) c_a = BLACK;
}
// map it into frame buffer
x = c; // restore coordiates if we were PUSHing
y = r;
if (topSegment.reverse ) x = nCols - x - 1;
if (topSegment.reverse_y) y = nRows - y - 1;
if (topSegment.transpose) std::swap(x,y); // swap X & Y if segment transposed
if (applyReverse) {
if (topSegment.reverse ) x = nCols - x - 1;
if (topSegment.reverse_y) y = nRows - y - 1;
if (topSegment.transpose) std::swap(x,y); // swap X & Y if segment transposed
}
// expand pixel
const unsigned groupLen = topSegment.groupLength();
if (groupLen == 1) {
setMirroredPixel(x, y, c_a, opacity);
} else {
@@ -1543,12 +1624,12 @@ void WS2812FX::blendSegment(const Segment &topSegment) const {
unsigned indxM = topSegment.stop - i - 1;
indxM += topSegment.offset; // offset/phase
if (indxM >= topSegment.stop) indxM -= length; // wrap
_pixels[indxM] = color_blend(_pixels[indxM], blend(c, _pixels[indxM]), o);
_pixels[indxM] = color_blend(_pixels[indxM], segblend(c, _pixels[indxM]), o);
if (_pixelCCT) _pixelCCT[indxM] = cct;
}
indx += topSegment.offset; // offset/phase
if (indx >= topSegment.stop) indx -= length; // wrap
_pixels[indx] = color_blend(_pixels[indx], blend(c, _pixels[indx]), o);
_pixels[indx] = color_blend(_pixels[indx], segblend(c, _pixels[indx]), o);
if (_pixelCCT) _pixelCCT[indx] = cct;
};
@@ -1563,19 +1644,20 @@ void WS2812FX::blendSegment(const Segment &topSegment) const {
int i = k;
// if we blend using "push" style we need to "shift" canvas to left or right
switch (blendingStyle) {
case BLEND_STYLE_PUSH_RIGHT: i = (i + offsetI) % nLen; break;
case BLEND_STYLE_PUSH_LEFT: i = (i - offsetI + nLen) % nLen; break;
case TRANSITION_PUSH_RIGHT: i = (i + offsetI) % nLen; break;
case TRANSITION_PUSH_LEFT: i = (i - offsetI + nLen) % nLen; break;
}
uint32_t c_a = BLACK;
if (i < vLen) c_a = seg->getPixelColorRaw(i); // will get clipped pixel from old segment or unclipped pixel from new segment
if (segO && blendingStyle == BLEND_STYLE_FADE && topSegment.mode != segO->mode && i < oLen) {
if (segO && blendingStyle == TRANSITION_FADE && topSegment.mode != segO->mode && i < oLen) {
// we need to blend old segment using fade as pixels are not clipped
c_a = color_blend16(c_a, segO->getPixelColorRaw(i), progInv);
} else if (blendingStyle != BLEND_STYLE_FADE) {
} else if (blendingStyle != TRANSITION_FADE) {
// if we have global brightness change (not On/Off change) we will ignore transition style and just fade brightness (see led.cpp)
// workaround for On/Off transition
// (bri != briT) && !bri => from On to Off
// (bri != briT) && bri => from Off to On
if ((!clipped && (bri != briT) && !bri) || (clipped && (bri != briT) && bri)) c_a = BLACK;
if ((briOld == 0 || bri == 0) && ((!clipped && (bri != briT) && !bri) || (clipped && (bri != briT) && bri))) c_a = BLACK;
}
// map into frame buffer
i = k; // restore index if we were PUSHing
@@ -1627,6 +1709,9 @@ void WS2812FX::show() {
int oldCCT = Bus::getCCT(); // store original CCT value (since it is global)
// when cctFromRgb is true we implicitly calculate WW and CW from RGB values (cct==-1)
if (cctFromRgb) BusManager::setSegmentCCT(-1);
// use color gamma correction if enabled, not in realtime mode with gamma disabled or currently overriding RT mode
bool useGammaCorrection = gammaCorrectCol && !(realtimeMode && arlsDisableGammaCorrection && !realtimeOverride);
for (size_t i = 0; i < totalLen; i++) {
// when correctWB is true setSegmentCCT() will convert CCT into K with which we can then
// correct/adjust RGB value according to desired CCT value, it will still affect actual WW/CW ratio
@@ -1635,8 +1720,8 @@ void WS2812FX::show() {
}
uint32_t c = _pixels[i]; // need a copy, do not modify _pixels directly (no byte access allowed on ESP32)
if(c > 0 && !(realtimeMode && arlsDisableGammaCorrection))
c = gamma32(c); // apply gamma correction if enabled note: applying gamma after brightness has too much color loss
if (c > 0 && useGammaCorrection)
c = gamma32(c); // apply gamma correction if enabled note: applying gamma after brightness has too much color loss
BusManager::setPixelColor(getMappedPixelIndex(i), c);
}
Bus::setCCT(oldCCT); // restore old CCT for ABL adjustments
@@ -1721,7 +1806,7 @@ void WS2812FX::setBrightness(uint8_t b, bool direct) {
BusManager::setBrightness(scaledBri(b));
if (!direct) {
unsigned long t = millis();
if (_segments[0].next_time > t + 22 && t - _lastShow > MIN_SHOW_DELAY) trigger(); //apply brightness change immediately if no refresh soon
if (t - _lastShow > min(_frametime, uint16_t(FRAMETIME_FIXED))) trigger(); //apply brightness change immediately if no refresh soon, but don't speed up above 42fps
}
}
@@ -1822,6 +1907,10 @@ void WS2812FX::resetSegments() {
if (isServicing()) return;
_segments.clear(); // destructs all Segment as part of clearing
_segments.emplace_back(0, isMatrix ? Segment::maxWidth : _length, 0, isMatrix ? Segment::maxHeight : 1);
if(_segments.size() == 0) {
_segments.emplace_back(); // if out of heap, create a default segment
errorFlag = ERR_NORAM_PX;
}
_segments.shrink_to_fit(); // just in case ...
_mainSegment = 0;
}
@@ -1844,7 +1933,7 @@ void WS2812FX::makeAutoSegments(bool forceReset) {
for (size_t i = s; i < BusManager::getNumBusses(); i++) {
const Bus *bus = BusManager::getBus(i);
if (!bus || !bus->isOk()) break;
if (!bus) break;
segStarts[s] = bus->getStart();
segStops[s] = segStarts[s] + bus->getLength();
@@ -1973,14 +2062,17 @@ bool WS2812FX::deserializeMap(unsigned n) {
customMappingSize = 0; // prevent use of mapping if anything goes wrong
currentLedmap = 0;
if (n == 0 || isFile) interfaceUpdateCallMode = CALL_MODE_WS_SEND; // schedule WS update (to inform UI)
uint32_t lengthTotalBefore = strip.getLengthTotal();
if (!isFile && n==0 && isMatrix) {
// 2D panel support creates its own ledmap (on the fly) if a ledmap.json does not exist
setUpMatrix();
if (strip.getLengthTotal() != lengthTotalBefore)
strip.updatePixelBuffer(); // allocate _pixels[] to match new length
return false;
}
if (!isFile || !requestJSONBufferLock(7)) return false;
if (!isFile || !requestJSONBufferLock(JSON_LOCK_LEDMAP)) return false;
StaticJsonDocument<64> filter;
filter[F("width")] = true;
@@ -2051,6 +2143,8 @@ bool WS2812FX::deserializeMap(unsigned n) {
}
releaseJSONBufferLock();
if (strip.getLengthTotal() != lengthTotalBefore)
strip.updatePixelBuffer(); // allocate _pixels[] to match new length
return (customMappingSize > 0);
}
File diff suppressed because it is too large Load Diff
+30 -11
View File
@@ -17,7 +17,7 @@
#include <stdint.h>
#include "wled.h"
#define PS_P_MAXSPEED 120 // maximum speed a particle can have (vx/vy is int8)
#define PS_P_MAXSPEED 120 // maximum speed a particle can have (vx/vy is int8), limiting below 127 to avoid overflows in collisions due to rounding errors
#define MAX_MEMIDLE 10 // max idle time (in frames) before memory is deallocated (if deallocated during an effect, it will crash!)
//#define WLED_DEBUG_PS // note: enabling debug uses ~3k of flash
@@ -103,7 +103,7 @@ typedef union {
// struct for additional particle settings (option)
typedef struct { // 2 bytes
uint8_t size; // particle size, 255 means 10 pixels in diameter, 0 means use global size (including single pixel rendering)
uint8_t size; // particle size, 255 means 10 pixels in diameter, set perParticleSize = true to enable
uint8_t forcecounter; // counter for applying forces to individual particles
} PSadvancedParticle;
@@ -143,7 +143,7 @@ public:
ParticleSystem2D(const uint32_t width, const uint32_t height, const uint32_t numberofparticles, const uint32_t numberofsources, const bool isadvanced = false, const bool sizecontrol = false); // constructor
// note: memory is allcated in the FX function, no deconstructor needed
void update(void); //update the particles according to set options and render to the matrix
void updateFire(const uint8_t intensity, const bool renderonly); // update function for fire, if renderonly is set, particles are not updated (required to fix transitions with frameskips)
void updateFire(const uint8_t intensity); // update function for fire
void updateSystem(void); // call at the beginning of every FX, updates pointers and dimensions
void particleMoveUpdate(PSparticle &part, PSparticleFlags &partFlags, PSsettings2D *options = NULL, PSadvancedParticle *advancedproperties = NULL); // move function
// particle emitters
@@ -190,16 +190,18 @@ public:
int32_t maxXpixel, maxYpixel; // last physical pixel that can be drawn to (FX can read this to read segment size if required), equal to width-1 / height-1
uint32_t numSources; // number of sources
uint32_t usedParticles; // number of particles used in animation, is relative to 'numParticles'
bool perParticleSize; // if true, uses individual particle sizes from advPartProps if available (disabled when calling setParticleSize())
//note: some variables are 32bit for speed and code size at the cost of ram
private:
//rendering functions
void render();
[[gnu::hot]] void renderParticle(const uint32_t particleindex, const uint8_t brightness, const CRGBW& color, const bool wrapX, const bool wrapY);
void renderLargeParticle(const uint32_t size, const uint32_t particleindex, const uint8_t brightness, const CRGBW& color, const bool wrapX, const bool wrapY);
//paricle physics applied by system if flags are set
void applyGravity(); // applies gravity to all particles
void handleCollisions();
[[gnu::hot]] void collideParticles(PSparticle &particle1, PSparticle &particle2, const int32_t dx, const int32_t dy, const uint32_t collDistSq);
void collideParticles(PSparticle &particle1, PSparticle &particle2, int32_t dx, int32_t dy, const uint32_t collDistSq, int32_t massratio1, int32_t massratio2);
void fireParticleupdate();
//utility functions
void updatePSpointers(const bool isadvanced, const bool sizecontrol); // update the data pointers to current segment data space
@@ -226,12 +228,26 @@ private:
uint8_t smearBlur; // 2D smeared blurring of full frame
};
void blur2D(uint32_t *colorbuffer, const uint32_t xsize, uint32_t ysize, const uint32_t xblur, const uint32_t yblur, const uint32_t xstart = 0, uint32_t ystart = 0, const bool isparticle = false);
// initialization functions (not part of class)
bool initParticleSystem2D(ParticleSystem2D *&PartSys, const uint32_t requestedsources, const uint32_t additionalbytes = 0, const bool advanced = false, const bool sizecontrol = false);
uint32_t calculateNumberOfParticles2D(const uint32_t pixels, const bool advanced, const bool sizecontrol);
uint32_t calculateNumberOfSources2D(const uint32_t pixels, const uint32_t requestedsources);
bool allocateParticleSystemMemory2D(const uint32_t numparticles, const uint32_t numsources, const bool advanced, const bool sizecontrol, const uint32_t additionalbytes);
// distance-based brightness for ellipse rendering, returns brightness (0-255) based on distance from ellipse center
inline uint8_t calculateEllipseBrightness(int32_t dx, int32_t dy, int32_t rxsq, int32_t rysq, uint8_t maxBrightness) {
// square the distances
uint32_t dx_sq = dx * dx;
uint32_t dy_sq = dy * dy;
uint32_t dist_sq = ((dx_sq << 8) / rxsq) + ((dy_sq << 8) / rysq); // normalized squared distance in fixed point: (dx²/rx²) * 256 + (dy²/ry²) * 256
if (dist_sq >= 256) return 0; // pixel is outside the ellipse, unit radius in fixed point: 256 = 1.0
//if (dist_sq <= 96) return maxBrightness; // core at full brightness
int32_t falloff = 256 - dist_sq;
return (maxBrightness * falloff) >> 8; // linear falloff
//return (maxBrightness * falloff * falloff) >> 16; // squared falloff for even softer edges
}
#endif // WLED_DISABLE_PARTICLESYSTEM2D
////////////////////////
@@ -301,9 +317,9 @@ typedef union {
// struct for additional particle settings (optional)
typedef struct {
uint8_t sat; //color saturation
uint8_t sat; // color saturation
uint8_t size; // particle size, 255 means 10 pixels in diameter, this overrides global size setting
uint8_t forcecounter;
uint8_t forcecounter; // counter for applying forces to individual particles
} PSadvancedParticle1D;
//struct for a particle source (20 bytes)
@@ -346,10 +362,11 @@ public:
void setColorByPosition(const bool enable);
void setMotionBlur(const uint8_t bluramount); // note: motion blur can only be used if 'particlesize' is set to zero
void setSmearBlur(const uint8_t bluramount); // enable 1D smeared blurring of full frame
void setParticleSize(const uint8_t size); //size 0 = 1 pixel, size 1 = 2 pixels, is overruled if advanced particle is used
void setParticleSize(const uint8_t size); // particle diameter: size 0 = 1 pixel, size 1 = 2 pixels, size = 255 = 10 pixels, disables per particle size control if called
void setGravity(int8_t force = 8);
void enableParticleCollisions(bool enable, const uint8_t hardness = 255);
PSparticle1D *particles; // pointer to particle array
PSparticleFlags1D *particleFlags; // pointer to particle flags array
PSsource1D *sources; // pointer to sources
@@ -360,16 +377,18 @@ public:
int32_t maxXpixel; // last physical pixel that can be drawn to (FX can read this to read segment size if required), equal to width-1
uint32_t numSources; // number of sources
uint32_t usedParticles; // number of particles used in animation, is relative to 'numParticles'
bool perParticleSize; // if true, uses individual particle sizes from advPartProps if available (disabled when calling setParticleSize())
private:
//rendering functions
void render(void);
[[gnu::hot]] void renderParticle(const uint32_t particleindex, const uint8_t brightness, const CRGBW &color, const bool wrap);
void renderParticle(const uint32_t particleindex, const uint8_t brightness, const CRGBW &color, const bool wrap);
void renderLargeParticle(const uint32_t size, const uint32_t particleindex, const uint8_t brightness, const CRGBW& color, const bool wrap);
//paricle physics applied by system if flags are set
void applyGravity(); // applies gravity to all particles
void handleCollisions();
[[gnu::hot]] void collideParticles(PSparticle1D &particle1, const PSparticleFlags1D &particle1flags, PSparticle1D &particle2, const PSparticleFlags1D &particle2flags, const int32_t dx, const uint32_t dx_abs, const uint32_t collisiondistance);
void collideParticles(uint32_t partIdx1, uint32_t partIdx2, int32_t dx, uint32_t collisiondistance);
//utility functions
void updatePSpointers(const bool isadvanced); // update the data pointers to current segment data space
@@ -397,5 +416,5 @@ bool initParticleSystem1D(ParticleSystem1D *&PartSys, const uint32_t requestedso
uint32_t calculateNumberOfParticles1D(const uint32_t fraction, const bool isadvanced);
uint32_t calculateNumberOfSources1D(const uint32_t requestedsources);
bool allocateParticleSystemMemory1D(const uint32_t numparticles, const uint32_t numsources, const bool isadvanced, const uint32_t additionalbytes);
void blur1D(uint32_t *colorbuffer, uint32_t size, uint32_t blur, uint32_t start);
#endif // WLED_DISABLE_PARTICLESYSTEM1D
+16
View File
@@ -15,6 +15,22 @@
#define NODE_TYPE_ID_ESP32S3 34
#define NODE_TYPE_ID_ESP32C3 35
// updated node types from the ESP Easy project
// https://github.com/letscontrolit/ESPEasy/blob/mega/src/src/DataTypes/NodeTypeID.h
//#define NODE_TYPE_ID_ESP32 33
//#define NODE_TYPE_ID_ESP32S2 34
//#define NODE_TYPE_ID_ESP32C3 35
//#define NODE_TYPE_ID_ESP32S3 36
#define NODE_TYPE_ID_ESP32C2 37
#define NODE_TYPE_ID_ESP32H2 38
#define NODE_TYPE_ID_ESP32C6 39
#define NODE_TYPE_ID_ESP32C61 40
#define NODE_TYPE_ID_ESP32C5 41
#define NODE_TYPE_ID_ESP32P4 42
#define NODE_TYPE_ID_ESP32P4r3 45
#define NODE_TYPE_ID_ESP32H21 43
#define NODE_TYPE_ID_ESP32H4 44
/*********************************************************************************************\
* NodeStruct
\*********************************************************************************************/
+91
View File
@@ -0,0 +1,91 @@
#pragma once
/*
asyncDNS.h - wrapper class for asynchronous DNS lookups using lwIP
by @dedehai, C++ improvements & hardening by @willmmiles
*/
#include <Arduino.h>
#include <atomic>
#include <memory>
#include <lwip/dns.h>
#include <lwip/err.h>
class AsyncDNS {
// C++14 shim
#if __cplusplus < 201402L
// Really simple C++11 shim for non-array case; implementation from cppreference.com
template<class T, class... Args>
static std::unique_ptr<T>
make_unique(Args&&... args)
{
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
#endif
public:
// note: passing the IP as a pointer to query() is not implemented because it is not thread-safe without mutexes
// with the IDF V4 bug external error handling is required anyway or dns can just stay stuck
enum class result { Idle, Busy, Success, Error };
// non-blocking query function to start DNS lookup
static std::shared_ptr<AsyncDNS> query(const char* hostname, std::shared_ptr<AsyncDNS> current = {}) {
if (!current || (current->_status == result::Busy)) {
current.reset(new AsyncDNS());
}
#if __cplusplus >= 201402L
using std::make_unique;
#endif
std::unique_ptr<std::shared_ptr<AsyncDNS>> callback_state = make_unique<std::shared_ptr<AsyncDNS> >(current);
if (!callback_state) return {};
current->_status = result::Busy;
err_t err = dns_gethostbyname(hostname, &current->_raw_addr, _dns_callback, callback_state.get());
if (err == ERR_OK) {
current->_status = result::Success; // result already in cache
} else if (err == ERR_INPROGRESS) {
callback_state.release(); // belongs to the callback now
} else {
current->_status = result::Error;
current->_errorcount++;
}
return current;
}
// get the IP once Success is returned
const IPAddress getIP() {
if (_status != result::Success) return IPAddress(0,0,0,0);
#ifdef ARDUINO_ARCH_ESP32
return IPAddress(_raw_addr.u_addr.ip4.addr);
#else
return IPAddress(_raw_addr.addr);
#endif
}
void reset() { _errorcount = 0; } // reset status and error count
const result status() { return _status; }
const uint16_t getErrorCount() { return _errorcount; }
private:
ip_addr_t _raw_addr;
std::atomic<result> _status { result::Idle };
uint16_t _errorcount = 0;
AsyncDNS(){}; // may not be explicitly instantiated - use query()
// callback for dns_gethostbyname(), called when lookup is complete or timed out
static void _dns_callback(const char *name, const ip_addr_t *ipaddr, void *arg) {
std::shared_ptr<AsyncDNS>* instance_ptr = reinterpret_cast<std::shared_ptr<AsyncDNS>*>(arg);
AsyncDNS& instance = **instance_ptr;
if (ipaddr) {
instance._raw_addr = *ipaddr;
instance._status = result::Success;
} else {
instance._status = result::Error; // note: if query timed out (~5s), DNS lookup is broken until WiFi connection is reset (IDF V4 bug)
instance._errorcount++;
}
delete instance_ptr;
}
};
+311 -277
View File
@@ -9,98 +9,45 @@
#include "src/dependencies/network/Network.h" // for isConnected() (& WiFi)
#include "driver/ledc.h"
#include "soc/ledc_struct.h"
#if !(defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3))
#define LEDC_MUTEX_LOCK() do {} while (xSemaphoreTake(_ledc_sys_lock, portMAX_DELAY) != pdPASS)
#define LEDC_MUTEX_UNLOCK() xSemaphoreGive(_ledc_sys_lock)
extern xSemaphoreHandle _ledc_sys_lock;
#else
#define LEDC_MUTEX_LOCK()
#define LEDC_MUTEX_UNLOCK()
#endif
#endif
#ifdef ESP8266
#include "core_esp8266_waveform.h"
#endif
#include "const.h"
#include "colors.h"
#include "pin_manager.h"
#include "bus_manager.h"
#include "bus_wrapper.h"
#include <bits/unique_ptr.h>
extern char cmDNS[];
extern bool cctICused;
extern bool useParallelI2S;
#include "wled.h"
// functions to get/set bits in an array - based on functions created by Brandon for GOL
// toDo : make this a class that's completely defined in a header file
bool getBitFromArray(const uint8_t* byteArray, size_t position) { // get bit value
size_t byteIndex = position / 8;
unsigned bitIndex = position % 8;
// note: these functions are automatically inline by the compiler
static inline bool getBitFromArray(const uint8_t* byteArray, size_t position) { // get bit value
size_t byteIndex = position >> 3; // divide by 8
unsigned bitIndex = position & 0x07; // modulo 8
uint8_t byteValue = byteArray[byteIndex];
return (byteValue >> bitIndex) & 1;
}
void setBitInArray(uint8_t* byteArray, size_t position, bool value) { // set bit - with error handling for nullptr
static inline void setBitInArray(uint8_t* byteArray, size_t position, bool value) { // set bit - with error handling for nullptr
//if (byteArray == nullptr) return;
size_t byteIndex = position / 8;
unsigned bitIndex = position % 8;
size_t byteIndex = position >> 3; // divide by 8
unsigned bitIndex = position & 0x07; // modulo 8
if (value)
byteArray[byteIndex] |= (1 << bitIndex);
else
byteArray[byteIndex] &= ~(1 << bitIndex);
}
size_t getBitArrayBytes(size_t num_bits) { // number of bytes needed for an array with num_bits bits
return (num_bits + 7) / 8;
static inline size_t getBitArrayBytes(size_t num_bits) { // number of bytes needed for an array with num_bits bits
return (num_bits + 7) >> 3;
}
void setBitArray(uint8_t* byteArray, size_t numBits, bool value) { // set all bits to same value
static inline void setBitArray(uint8_t* byteArray, size_t numBits, bool value) { // set all bits to same value
if (byteArray == nullptr) return;
size_t len = getBitArrayBytes(numBits);
if (value) memset(byteArray, 0xFF, len);
else memset(byteArray, 0x00, len);
}
//colors.cpp
uint32_t colorBalanceFromKelvin(uint16_t kelvin, uint32_t rgb);
//udp.cpp
uint8_t realtimeBroadcast(uint8_t type, IPAddress client, uint16_t length, const byte *buffer, uint8_t bri=255, bool isRGBW=false);
//util.cpp
// memory allocation wrappers
extern "C" {
// prefer DRAM over PSRAM (if available) in d_ alloc functions
void *d_malloc(size_t);
void *d_calloc(size_t, size_t);
void *d_realloc_malloc(void *ptr, size_t size);
#ifndef ESP8266
inline void d_free(void *ptr) { heap_caps_free(ptr); }
#else
inline void d_free(void *ptr) { free(ptr); }
#endif
#if defined(BOARD_HAS_PSRAM)
// prefer PSRAM over DRAM in p_ alloc functions
void *p_malloc(size_t);
void *p_calloc(size_t, size_t);
void *p_realloc_malloc(void *ptr, size_t size);
inline void p_free(void *ptr) { heap_caps_free(ptr); }
#else
#define p_malloc d_malloc
#define p_calloc d_calloc
#define p_free d_free
#endif
}
//color mangling macros
#define RGBW32(r,g,b,w) (uint32_t((byte(w) << 24) | (byte(r) << 16) | (byte(g) << 8) | (byte(b))))
#define R(c) (byte((c) >> 16))
#define G(c) (byte((c) >> 8))
#define B(c) (byte(c))
#define W(c) (byte((c) >> 24))
static ColorOrderMap _colorOrderMap = {};
bool ColorOrderMap::add(uint16_t start, uint16_t len, uint8_t colorOrder) {
@@ -131,42 +78,67 @@ void Bus::calculateCCT(uint32_t c, uint8_t &ww, uint8_t &cw) {
cct = (approximateKelvinFromRGB(c) - 1900) >> 5; // convert K (from RGB value) to relative format
}
//0 - linear (CCT 127 = 50% warm, 50% cold), 127 - additive CCT blending (CCT 127 = 100% warm, 100% cold)
if (cct < _cctBlend) ww = 255;
else ww = ((255-cct) * 255) / (255 - _cctBlend);
if ((255-cct) < _cctBlend) cw = 255;
else cw = (cct * 255) / (255 - _cctBlend);
// CCT blending modes (_cctBlend):
// blend<0: ww: ▓▓▒░__ | blend=0: ww: ▓▒▒░░ | blend>0 ww: ▓▓▓▒░
// cw: __░▒▓▓ | cw: ░░▒▒▓ | cw: ░▒▓▓▓
int32_t ww_val, cw_val;
if (_cctBlend < 0) {
uint16_t range = 255 - 2 * (uint16_t)(-_cctBlend);
if (range > 255) range = 255; // prevent overflow
ww_val = range ? ((int32_t)(255 + _cctBlend - cct) * 255) / range : (cct < 128 ? 255 : 0); // exclusive blending
cw_val = 255 - ww_val;
} else {
ww_val = _cctBlend ? ((int32_t)(255 - cct) * 255) / (255 - _cctBlend) : 255 - cct; // additive blending
cw_val = _cctBlend ? ((int32_t) cct * 255) / (255 - _cctBlend) : cct;
}
ww = (uint8_t)(ww_val < 0 ? 0 : ww_val > 255 ? 255 : ww_val);
cw = (uint8_t)(cw_val < 0 ? 0 : cw_val > 255 ? 255 : cw_val);
ww = (w * ww) / 255; //brightness scaling
cw = (w * cw) / 255;
}
uint32_t Bus::autoWhiteCalc(uint32_t c) const {
// calculates white channel and CCT values based on given settings
uint32_t Bus::autoWhiteCalc(uint32_t c, uint8_t &ww, uint8_t &cw) const {
unsigned aWM = _autoWhiteMode;
if (_gAWM < AW_GLOBAL_DISABLED) aWM = _gAWM;
if (aWM == RGBW_MODE_MANUAL_ONLY) return c;
CRGBW cIn = c; // save original color for CCT calculation
unsigned w = W(c);
//ignore auto-white calculation if w>0 and mode DUAL (DUAL behaves as BRIGHTER if w==0)
if (w > 0 && aWM == RGBW_MODE_DUAL) return c;
unsigned r = R(c);
unsigned g = G(c);
unsigned b = B(c);
if (aWM == RGBW_MODE_MAX) return RGBW32(r, g, b, r > g ? (r > b ? r : b) : (g > b ? g : b)); // brightest RGB channel
w = r < g ? (r < b ? r : b) : (g < b ? g : b);
if (aWM == RGBW_MODE_AUTO_ACCURATE) { r -= w; g -= w; b -= w; } //subtract w in ACCURATE mode
return RGBW32(r, g, b, w);
if (aWM != RGBW_MODE_MANUAL_ONLY) {
unsigned r = R(c); // note: using uint8_t generates larger code
unsigned g = G(c);
unsigned b = B(c);
if (aWM == RGBW_MODE_DUAL && w > 0) {
//ignore auto-white calculation if w>0 and mode DUAL (DUAL behaves as BRIGHTER if w==0)
} else if (aWM == RGBW_MODE_MAX) {
w = r > g ? (r > b ? r : b) : (g > b ? g : b); // brightest RGB channel
} else {
w = r < g ? (r < b ? r : b) : (g < b ? g : b); // darkest RGB channel
if (aWM == RGBW_MODE_AUTO_ACCURATE) { r -= w; g -= w; b -= w; } //subtract w in ACCURATE mode
}
c = RGBW32(r, g, b, w);
}
if (_hasCCT) {
cIn.w = w; // need original rgb values in case CCT is derived from RGB
calculateCCT(cIn, ww, cw);
}
return c;
}
BusDigital::BusDigital(const BusConfig &bc, uint8_t nr)
BusDigital::BusDigital(const BusConfig &bc)
: Bus(bc.type, bc.start, bc.autoWhite, bc.count, bc.reversed, (bc.refreshReq || bc.type == TYPE_TM1814))
, _skip(bc.skipAmount) //sacrificial pixels
, _colorOrder(bc.colorOrder)
, _milliAmpsPerLed(bc.milliAmpsPerLed)
, _milliAmpsMax(bc.milliAmpsMax)
, _driverType(bc.driverType) // Store driver preference (0=RMT, 1=I2S)
{
DEBUGBUS_PRINTLN(F("Bus: Creating digital bus."));
if (!isDigital(bc.type) || !bc.count) { DEBUGBUS_PRINTLN(F("Not digial or empty bus!")); return; }
_iType = bc.iType; // reuse the iType that was determined by polyBus in getI() in finalizeInit()
if (_iType == I_NONE) { DEBUGBUS_PRINTLN(F("Incorrect iType!")); return; }
if (!PinManager::allocatePin(bc.pins[0], true, PinOwner::BusDigital)) { DEBUGBUS_PRINTLN(F("Pin 0 allocated!")); return; }
_frequencykHz = 0U;
_colorSum = 0;
@@ -180,28 +152,30 @@ BusDigital::BusDigital(const BusConfig &bc, uint8_t nr)
_pins[1] = bc.pins[1];
_frequencykHz = bc.frequency ? bc.frequency : 2000U; // 2MHz clock if undefined
}
_iType = PolyBus::getI(bc.type, _pins, nr);
if (_iType == I_NONE) { DEBUGBUS_PRINTLN(F("Incorrect iType!")); return; }
_hasRgb = hasRGB(bc.type);
_hasWhite = hasWhite(bc.type);
_hasCCT = hasCCT(bc.type);
uint16_t lenToCreate = bc.count;
if (bc.type == TYPE_WS2812_1CH_X3) lenToCreate = NUM_ICS_WS2812_1CH_3X(bc.count); // only needs a third of "RGB" LEDs for NeoPixelBus
_busPtr = PolyBus::create(_iType, _pins, lenToCreate + _skip, nr);
_busPtr = PolyBus::create(_iType, _pins, lenToCreate + _skip);
_valid = (_busPtr != nullptr) && bc.count > 0;
// fix for wled#4759
if (_valid) for (unsigned i = 0; i < _skip; i++) {
PolyBus::setPixelColor(_busPtr, _iType, i, 0, COL_ORDER_GRB); // set sacrificial pixels to black (CO does not matter here)
}
DEBUGBUS_PRINTF_P(PSTR("Bus: %successfully inited #%u (len:%u, type:%u (RGB:%d, W:%d, CCT:%d), pins:%u,%u [itype:%u] mA=%d/%d)\n"),
_valid?"S":"Uns",
(int)nr,
else {
cleanup();
}
DEBUGBUS_PRINTF_P(PSTR("Bus len:%u, type:%u (RGB:%d, W:%d, CCT:%d), pins:%u,%u [itype:%u, driver:%s] mA=%d/%d %s\n"),
(int)bc.count,
(int)bc.type,
(int)_hasRgb, (int)_hasWhite, (int)_hasCCT,
(unsigned)_pins[0], is2Pin(bc.type)?(unsigned)_pins[1]:255U,
(unsigned)_iType,
(int)_milliAmpsPerLed, (int)_milliAmpsMax
isI2S() ? "I2S" : "RMT",
(int)_milliAmpsPerLed, (int)_milliAmpsMax,
_valid ? " " : "FAILED"
);
}
@@ -252,15 +226,20 @@ void BusDigital::applyBriLimit(uint8_t newBri) {
if (newBri < 255) {
_NPBbri = newBri; // store value so it can be updated in show() (must be updated even if ABL is not used)
uint8_t cctWW = 0, cctCW = 0;
uint16_t wwcw = 0;
unsigned hwLen = _len;
if (_type == TYPE_WS2812_1CH_X3) hwLen = NUM_ICS_WS2812_1CH_3X(_len); // only needs a third of "RGB" LEDs for NeoPixelBus
for (unsigned i = 0; i < hwLen; i++) {
uint8_t co = _colorOrderMap.getPixelColorOrder(i+_start, _colorOrder); // need to revert color order for correct color scaling and CCT calc in case white is swapped
uint32_t c = PolyBus::getPixelColor(_busPtr, _iType, i, co);
uint32_t c = PolyBus::getPixelColor(_busPtr, _iType, i, co); // Note: if ABL would be calculated as a seperate loop (as it was before) it is slower but could use original color, making it more color-accurate
if (hasCCT()) {
uint8_t cctWW, cctCW;
Bus::calculateCCT(c, cctWW, cctCW); // calculate CCT before fade (more accurate) | Note: if using "accurate" white calculation mode, approximateKelvinFromRGB can be very inaccurate (white is subtracted)
wwcw = ((cctCW + 1) * newBri) & 0xFF00; // apply brightness to CCT (leave it in upper byte for 16bit NeoPixelBus value)
wwcw |= ((cctWW + 1) * newBri) >> 8;
}
c = color_fade(c, newBri, true); // apply additional dimming note: using inline version is a bit faster but overhead of getPixelColor() dominates the speed impact by far
if (hasCCT()) Bus::calculateCCT(c, cctWW, cctCW);
PolyBus::setPixelColor(_busPtr, _iType, i, c, co, (cctCW<<8) | cctWW); // repaint all pixels with new brightness
PolyBus::setPixelColor(_busPtr, _iType, i, c, co, wwcw); // repaint all pixels with new brightness
}
}
@@ -287,12 +266,21 @@ void BusDigital::setStatusPixel(uint32_t c) {
}
}
// note: using WLED_O2_ATTR makes this function ~7% faster at the expense of 600 bytes of flash
void IRAM_ATTR BusDigital::setPixelColor(unsigned pix, uint32_t c) {
if (!_valid) return;
if (hasWhite()) c = autoWhiteCalc(c);
if (Bus::_cct >= 1900) c = colorBalanceFromKelvin(Bus::_cct, c); //color correction from CCT
uint8_t cctWW = 0, cctCW = 0;
uint16_t wwcw = 0;
if (hasWhite()) c = autoWhiteCalc(c, cctWW, cctCW);
c = color_fade(c, _bri, true); // apply brightness
if (hasCCT()) {
wwcw = ((cctCW + 1) * _bri) & 0xFF00; // apply brightness to CCT (store CW in upper byte)
wwcw |= ((cctWW + 1) * _bri) >> 8;
if (_type == TYPE_WS2812_WWA) c = RGBW32(wwcw, wwcw >> 8, 0, W(c)); // ww,cw, 0, w
}
if (BusManager::_useABL) {
// if using ABL, sum all color channels to estimate current and limit brightness in show()
uint8_t r = R(c), g = G(c), b = B(c);
@@ -316,13 +304,7 @@ void IRAM_ATTR BusDigital::setPixelColor(unsigned pix, uint32_t c) {
case 2: c = RGBW32(R(cOld), G(cOld), W(c) , 0); break;
}
}
uint16_t wwcw = 0;
if (hasCCT()) {
uint8_t cctWW = 0, cctCW = 0;
Bus::calculateCCT(c, cctWW, cctCW);
wwcw = (cctCW<<8) | cctWW;
if (_type == TYPE_WS2812_WWA) c = RGBW32(cctWW, cctCW, 0, W(c));
}
PolyBus::setPixelColor(_busPtr, _iType, pix, c, co, wwcw);
}
@@ -369,29 +351,32 @@ void BusDigital::setColorOrder(uint8_t colorOrder) {
// credit @willmmiles & @netmindz https://github.com/wled/WLED/pull/4056
std::vector<LEDType> BusDigital::getLEDTypes() {
return {
{TYPE_WS2812_RGB, "D", PSTR("WS281x")},
{TYPE_WS2812_RGB, "D", PSTR("WS281x RGB")},
{TYPE_WS2811_400KHZ, "D", PSTR("400kHz RGB")},
{TYPE_TM1829, "D", PSTR("TM1829 RGB")},
{TYPE_UCS8903, "D", PSTR("UCS8903 RGB")},
{TYPE_APA106, "D", PSTR("APA106/PL9823 RGB")},
{TYPE_TM1914, "D", PSTR("TM1914 RGB")},
{TYPE_SK6812_RGBW, "D", PSTR("SK6812/WS2814 RGBW")},
{TYPE_TM1814, "D", PSTR("TM1814")},
{TYPE_WS2811_400KHZ, "D", PSTR("400kHz")},
{TYPE_TM1829, "D", PSTR("TM1829")},
{TYPE_UCS8903, "D", PSTR("UCS8903")},
{TYPE_APA106, "D", PSTR("APA106/PL9823")},
{TYPE_TM1914, "D", PSTR("TM1914")},
{TYPE_FW1906, "D", PSTR("FW1906 GRBCW")},
{TYPE_UCS8904, "D", PSTR("UCS8904 RGBW")},
{TYPE_WS2805, "D", PSTR("WS2805 RGBCW")},
{TYPE_SM16825, "D", PSTR("SM16825 RGBCW")},
{TYPE_TM1814, "D", PSTR("TM1814 RGBW")},
{TYPE_FW1906, "D", PSTR("FW1906/WS2811 RGBCCT")},
{TYPE_WS2805, "D", PSTR("WS2805 RGBCCT")},
{TYPE_SM16825, "D", PSTR("SM16825 RGBCCT")},
{TYPE_WS2812_1CH_X3, "D", PSTR("WS2811 White")},
//{TYPE_WS2812_2CH_X3, "D", PSTR("WS281x CCT")}, // not implemented
{TYPE_WS2812_WWA, "D", PSTR("WS281x WWA")}, // amber ignored
{TYPE_WS2801, "2P", PSTR("WS2801")},
{TYPE_APA102, "2P", PSTR("APA102")},
{TYPE_LPD8806, "2P", PSTR("LPD8806")},
{TYPE_LPD6803, "2P", PSTR("LPD6803")},
{TYPE_P9813, "2P", PSTR("PP9813")},
{TYPE_WS2801, "2P", PSTR("WS2801 RGB")},
{TYPE_APA102, "2P", PSTR("APA102 RGB")},
{TYPE_LPD8806, "2P", PSTR("LPD8806 RGB")},
{TYPE_LPD6803, "2P", PSTR("LPD6803 RGB")},
{TYPE_P9813, "2P", PSTR("P9813 RGB")},
};
}
bool BusDigital::isI2S() {
return (_iType & 0x01) == 0; // I2S types have even iType values
}
void BusDigital::begin() {
if (!_valid) return;
PolyBus::begin(_busPtr, _iType, _pins, _frequencykHz);
@@ -484,11 +469,13 @@ BusPwm::BusPwm(const BusConfig &bc)
void BusPwm::setPixelColor(unsigned pix, uint32_t c) {
if (pix != 0 || !_valid) return; //only react to first pixel
if (_type != TYPE_ANALOG_3CH) c = autoWhiteCalc(c);
if (Bus::_cct >= 1900 && (_type == TYPE_ANALOG_3CH || _type == TYPE_ANALOG_4CH)) {
c = colorBalanceFromKelvin(Bus::_cct, c); //color correction from CCT
}
uint8_t cctWW, cctCW;
if (_type != TYPE_ANALOG_3CH) c = autoWhiteCalc(c, cctWW, cctCW);
uint8_t r = R(c), g = G(c), b = B(c), w = W(c);
// note: no color scaling, brightness is applied in show()
switch (_type) {
case TYPE_ANALOG_1CH: //one channel (white), relies on auto white calculation
@@ -499,14 +486,18 @@ void BusPwm::setPixelColor(unsigned pix, uint32_t c) {
_data[0] = w;
_data[1] = Bus::_cct < 0 || Bus::_cct > 255 ? 127 : Bus::_cct;
} else {
Bus::calculateCCT(c, _data[0], _data[1]);
_data[0] = cctWW;
_data[1] = cctCW;
}
break;
case TYPE_ANALOG_5CH: //RGB + warm white + cold white
if (cctICused)
_data[4] = Bus::_cct < 0 || Bus::_cct > 255 ? 127 : Bus::_cct;
else
Bus::calculateCCT(c, w, _data[4]);
else {
w = cctWW;
_data[4] = cctCW;
}
// fall through to set RGBW channels
case TYPE_ANALOG_4CH: //RGBW
_data[3] = w;
case TYPE_ANALOG_3CH: //standard dumb RGB
@@ -573,7 +564,7 @@ void BusPwm::show() {
unsigned duty = (_data[i] * pwmBri) / 255;
unsigned deadTime = 0;
if (_type == TYPE_ANALOG_2CH && Bus::_cctBlend == 0) {
if (_type == TYPE_ANALOG_2CH && Bus::_cctBlend <= 0) {
// add dead time between signals (when using dithering, two full 8bit pulses are required)
deadTime = (1+dithering) << bitShift;
// we only need to take care of shortening the signal at (almost) full brightness otherwise pulses may overlap
@@ -619,7 +610,7 @@ std::vector<LEDType> BusPwm::getLEDTypes() {
{TYPE_ANALOG_2CH, "AA", PSTR("PWM CCT")},
{TYPE_ANALOG_3CH, "AAA", PSTR("PWM RGB")},
{TYPE_ANALOG_4CH, "AAAA", PSTR("PWM RGBW")},
{TYPE_ANALOG_5CH, "AAAAA", PSTR("PWM RGB+CCT")},
{TYPE_ANALOG_5CH, "AAAAA", PSTR("PWM RGBCCT")},
//{TYPE_ANALOG_6CH, "AAAAAA", PSTR("PWM RGB+DCCT")}, // unimplementable ATM
};
}
@@ -662,9 +653,7 @@ BusOnOff::BusOnOff(const BusConfig &bc)
void BusOnOff::setPixelColor(unsigned pix, uint32_t c) {
if (pix != 0 || !_valid) return; //only react to first pixel
c = autoWhiteCalc(c);
uint8_t r = R(c), g = G(c), b = B(c), w = W(c);
_data = bool(r|g|b|w) && bool(_bri) ? 0xFF : 0;
_data = (c > 0) && bool(_bri) ? 0xFF : 0; // if any color channel is on and brightness is not zero, set to on
}
uint32_t BusOnOff::getPixelColor(unsigned pix) const {
@@ -724,7 +713,8 @@ BusNetwork::BusNetwork(const BusConfig &bc)
void BusNetwork::setPixelColor(unsigned pix, uint32_t c) {
if (!_valid || pix >= _len) return;
if (_hasWhite) c = autoWhiteCalc(c);
uint8_t ww, cw; // dummy, unused
if (_hasWhite) c = autoWhiteCalc(c, ww, cw);
if (Bus::_cct >= 1900) c = colorBalanceFromKelvin(Bus::_cct, c); //color correction from CCT
unsigned offset = pix * _UDPchannels;
_data[offset] = R(c);
@@ -753,13 +743,23 @@ size_t BusNetwork::getPins(uint8_t* pinArray) const {
#ifdef ARDUINO_ARCH_ESP32
void BusNetwork::resolveHostname() {
static unsigned long nextResolve = 0;
if (Network.isConnected() && millis() > nextResolve && _hostname.length() > 0) {
nextResolve = millis() + 600000; // resolve only every 10 minutes
static std::shared_ptr<AsyncDNS> DNSlookup; // TODO: make this dynamic? requires to handle the callback properly
if (Network.isConnected()) {
IPAddress clnt;
if (strlen(cmDNS) > 0) clnt = MDNS.queryHost(_hostname);
else WiFi.hostByName(_hostname.c_str(), clnt);
if (clnt != IPAddress()) _client = clnt;
if (strlen(cmDNS) > 0) {
clnt = MDNS.queryHost(_hostname);
if (clnt != IPAddress()) _client = clnt; // update client IP if not null
}
else {
int timeout = 5000; // 5 seconds timeout
DNSlookup = AsyncDNS::query(_hostname.c_str(), DNSlookup); // start async DNS query
while (DNSlookup->status() == AsyncDNS::result::Busy && timeout-- > 0) {
delay(1);
}
if (DNSlookup->status() == AsyncDNS::result::Success) {
_client = DNSlookup->getIP(); // update client IP
}
}
}
}
#endif
@@ -800,46 +800,47 @@ BusHub75Matrix::BusHub75Matrix(const BusConfig &bc) : Bus(bc.type, bc.start, bc.
_valid = false;
_hasRgb = true;
_hasWhite = false;
virtualDisp = nullptr; // todo: this should be solved properly, can cause memory leak (if omitted here, nothing seems to work)
// aliases for easier reading
uint8_t panelWidth = bc.pins[0];
uint8_t panelHeight = bc.pins[1];
uint8_t chainLength = bc.pins[2];
_rows = bc.pins[3];
_cols = bc.pins[4];
mxconfig.double_buff = false; // Use our own memory-optimised buffer rather than the driver's own double-buffer
// mxconfig.driver = HUB75_I2S_CFG::ICN2038S; // experimental - use specific shift register driver
// mxconfig.driver = HUB75_I2S_CFG::FM6124; // try this driver in case you panel stays dark, or when colors look too pastel
// Other possible shiftreg drivers: HUB75_I2S_CFG::FM6126A, HUB75_I2S_CFG::ICN2038S, HUB75_I2S_CFG::MBI5124, HUB75_I2S_CFG::DP3246
// mxconfig.latch_blanking = 3;
// mxconfig.i2sspeed = HUB75_I2S_CFG::HZ_10M; // experimental - 5MHZ should be enugh, but colours looks slightly better at 10MHz
//mxconfig.min_refresh_rate = 90;
//mxconfig.min_refresh_rate = 120;
mxconfig.clkphase = bc.reversed;
// mxconfig.min_refresh_rate = 90;
// mxconfig.min_refresh_rate = 120;
virtualDisp = nullptr;
mxconfig.clkphase = bc.reversed;
// allow chain length up to 4, limit to prevent bad data from preventing boot due to low memory
mxconfig.chain_length = max((uint8_t) 1, min(chainLength, (uint8_t) 4));
if (mxconfig.mx_height >= 64 && (mxconfig.chain_length > 1)) {
#if defined(BOARD_HAS_PSRAM) // limitation to one panel only applies to boards without PSRAM
if (!psramFound() || ESP.getPsramSize() == 0) // PSRAM sanity check
#endif
{
DEBUGBUS_PRINTLN(F("WARNING, only single panel can be used of 64 pixel boards due to memory"));
mxconfig.chain_length = 1;
}
}
if (bc.type == TYPE_HUB75MATRIX_HS) {
mxconfig.mx_width = min((uint8_t) 64, bc.pins[0]);
mxconfig.mx_height = min((uint8_t) 64, bc.pins[1]);
// Disable chains of panels for now, incomplete UI changes
// if(bc.pins[2] > 1 && bc.pins[3] != 0 && bc.pins[4] != 0 && bc.pins[3] != 255 && bc.pins[4] != 255) {
// virtualDisp = new VirtualMatrixPanel((*display), bc.pins[3], bc.pins[4], mxconfig.mx_width, mxconfig.mx_height, CHAIN_BOTTOM_LEFT_UP);
// }
mxconfig.mx_width = min((uint8_t) 64, panelWidth); // TODO: UI limit is 128, this limits to 64
mxconfig.mx_height = min((uint8_t) 64, panelHeight);
} else if (bc.type == TYPE_HUB75MATRIX_QS) {
mxconfig.mx_width = min((uint8_t) 64, bc.pins[0]) * 2;
mxconfig.mx_height = min((uint8_t) 64, bc.pins[1]) / 2;
virtualDisp = new VirtualMatrixPanel((*display), 1, 1, bc.pins[0], bc.pins[1]);
virtualDisp->setRotation(0);
switch(bc.pins[1]) {
case 16:
virtualDisp->setPhysicalPanelScanRate(FOUR_SCAN_16PX_HIGH);
break;
case 32:
virtualDisp->setPhysicalPanelScanRate(FOUR_SCAN_32PX_HIGH);
break;
case 64:
virtualDisp->setPhysicalPanelScanRate(FOUR_SCAN_64PX_HIGH);
break;
default:
DEBUGBUS_PRINTLN("Unsupported height");
return;
}
_isVirtual = true;
mxconfig.mx_width = min((uint8_t) 64, panelWidth) * 2;
mxconfig.mx_height = min((uint8_t) 64, panelHeight) / 2;
mxconfig.driver = HUB75_I2S_CFG::FM6124; // use FM6124 for "outdoor" 4-scan panels - workaround until we can make the driver user-configurable
} else {
DEBUGBUS_PRINTLN("Unknown type");
return;
@@ -853,12 +854,6 @@ BusHub75Matrix::BusHub75Matrix(const BusConfig &bc) : Bus(bc.type, bc.start, bc.
} else mxconfig.setPixelColorDepthBits(8);
#endif
mxconfig.chain_length = max((uint8_t) 1, min(bc.pins[2], (uint8_t) 4)); // prevent bad data preventing boot due to low memory
if(mxconfig.mx_height >= 64 && (mxconfig.chain_length > 1)) {
DEBUGBUS_PRINTLN("WARNING, only single panel can be used of 64 pixel boards due to memory");
mxconfig.chain_length = 1;
}
// HUB75_I2S_CFG::i2s_pins _pins={R1_PIN, G1_PIN, B1_PIN, R2_PIN, G2_PIN, B2_PIN, A_PIN, B_PIN, C_PIN, D_PIN, E_PIN, LAT_PIN, OE_PIN, CLK_PIN};
@@ -869,6 +864,14 @@ BusHub75Matrix::BusHub75Matrix(const BusConfig &bc) : Bus(bc.type, bc.start, bc.
DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA - Matrix Portal S3 config");
mxconfig.gpio = { 42, 41, 40, 38, 39, 37, 45, 36, 48, 35, 21, 47, 14, 2 };
#elif defined(HD_WF2_PINOUT) // Huidu HD-WF2 ESP32-S3 (no PSRAM)
// https://www.aliexpress.com/item/1005002258734810.html
// https://github.com/mrcodetastic/ESP32-HUB75-MatrixPanel-DMA/issues/433
DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA - HD-WF2 S3 config");
// HUB75_I2S_CFG::i2s_pins _pins={R1_PIN, G1_PIN, B1_PIN, R2_PIN, G2_PIN, B2_PIN, A_PIN, B_PIN, C_PIN, D_PIN, E_PIN, LAT_PIN, OE_PIN, CLK_PIN};
mxconfig.gpio = { 2, 6, 10, 3, 7, 11, 39, 38, 37, 36, 21, 33, 35, 34 };
#elif defined(CONFIG_IDF_TARGET_ESP32S3) && defined(BOARD_HAS_PSRAM)// ESP32-S3 with PSRAM
#if defined(MOONHUB_S3_PINOUT)
@@ -915,9 +918,9 @@ BusHub75Matrix::BusHub75Matrix(const BusConfig &bc) : Bus(bc.type, bc.start, bc.
return;
}
if(bc.colorOrder == COL_ORDER_RGB) {
if (bc.colorOrder == COL_ORDER_RGB) {
DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA = Default color order (RGB)");
} else if(bc.colorOrder == COL_ORDER_BGR) {
} else if (bc.colorOrder == COL_ORDER_BGR) {
DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA = color order BGR");
int8_t tmpPin;
tmpPin = mxconfig.gpio.r1;
@@ -944,21 +947,27 @@ BusHub75Matrix::BusHub75Matrix(const BusConfig &bc) : Bus(bc.type, bc.start, bc.
return;
}
this->_len = (display->width() * display->height());
this->_len = (display->width() * display->height()); // note: this returns correct number of pixels but incorrect dimensions if using virtual display (updated below)
DEBUGBUS_PRINTF("Length: %u\n", _len);
if(this->_len >= MAX_LEDS) {
if (this->_len >= MAX_LEDS) {
DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA Too many LEDS - playing safe");
return;
}
DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA created");
// let's adjust default brightness
display->setBrightness8(25); // range is 0-255, 0 - 0%, 255 - 100%
// as noted in HUB75_I2S_DMA library, some panels can show ghosting if set higher than 239, so let users override at compile time
#ifndef WLED_HUB75_MAX_BRIGHTNESS
#define WLED_HUB75_MAX_BRIGHTNESS 255
#endif
// let's adjust default brightness (128), brightness scaling is handled by WLED
//display->setBrightness8(WLED_HUB75_MAX_BRIGHTNESS); // range is 0-255, 0 - 0%, 255 - 100%
delay(24); // experimental
DEBUGBUS_PRINT(F("heap usage: ")); DEBUGBUS_PRINTLN(lastHeap - ESP.getFreeHeap());
// Allocate memory and start DMA display
if( not display->begin() ) {
if (!display->begin() ) {
DEBUGBUS_PRINTLN("****** MatrixPanel_I2S_DMA !KABOOM! I2S memory allocation failed ***********");
DEBUGBUS_PRINT(F("heap usage: ")); DEBUGBUS_PRINTLN(lastHeap - ESP.getFreeHeap());
return;
@@ -971,10 +980,10 @@ BusHub75Matrix::BusHub75Matrix(const BusConfig &bc) : Bus(bc.type, bc.start, bc.
display->clearScreen(); // initially clear the screen buffer
DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA clear ok");
if (_ledBuffer) free(_ledBuffer); // should not happen
if (_ledsDirty) free(_ledsDirty); // should not happen
if (_ledBuffer) d_free(_ledBuffer); // should not happen
if (_ledsDirty) d_free(_ledsDirty); // should not happen
DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA allocate memory");
_ledsDirty = (byte*) malloc(getBitArrayBytes(_len)); // create LEDs dirty bits
_ledsDirty = (byte*) d_malloc(getBitArrayBytes(_len)); // create LEDs dirty bits
DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA allocate memory ok");
if (_ledsDirty == nullptr) {
@@ -987,18 +996,49 @@ BusHub75Matrix::BusHub75Matrix(const BusConfig &bc) : Bus(bc.type, bc.start, bc.
}
setBitArray(_ledsDirty, _len, false); // reset dirty bits
if (mxconfig.double_buff == false) {
_ledBuffer = (CRGB*) calloc(_len, sizeof(CRGB)); // create LEDs buffer (initialized to BLACK)
}
// create LEDs buffer (initialized to BLACK), prefer DRAM if enough heap is available (faster in case global _pixels buffer is in PSRAM as not both will fit the cache)
_ledBuffer = static_cast<CRGB*>(allocate_buffer(_len * sizeof(CRGB), BFRALLOC_PREFER_DRAM | BFRALLOC_CLEAR));
}
PANEL_CHAIN_TYPE chainType = CHAIN_NONE; // default for quarter-scan panels that do not use chaining
// chained panels with cols and rows define need the virtual display driver, so do quarter-scan panels
if (chainLength > 1 && (_rows > 1 || _cols > 1) || bc.type == TYPE_HUB75MATRIX_QS) {
_isVirtual = true;
chainType = CHAIN_BOTTOM_LEFT_UP; // TODO: is there any need to support other chaining types?
DEBUGBUS_PRINTF_P(PSTR("Using virtual matrix: %ux%u panels of %ux%u pixels\n"), _cols, _rows, mxconfig.mx_width, mxconfig.mx_height);
}
else {
_isVirtual = false;
}
if (_isVirtual) {
virtualDisp = new VirtualMatrixPanel((*display), _rows, _cols, mxconfig.mx_width, mxconfig.mx_height, chainType);
virtualDisp->setRotation(0);
if (bc.type == TYPE_HUB75MATRIX_QS) {
switch(panelHeight) {
case 16:
virtualDisp->setPhysicalPanelScanRate(FOUR_SCAN_16PX_HIGH);
break;
case 32:
virtualDisp->setPhysicalPanelScanRate(FOUR_SCAN_32PX_HIGH);
break;
case 64:
virtualDisp->setPhysicalPanelScanRate(FOUR_SCAN_64PX_HIGH);
break;
default:
DEBUGBUS_PRINTLN("Unsupported height");
cleanup();
return;
}
}
}
if (_valid) {
_panelWidth = virtualDisp ? virtualDisp->width() : display->width(); // cache width - it will never change
}
DEBUGBUS_PRINT(F("MatrixPanel_I2S_DMA "));
DEBUGBUS_PRINTF("%sstarted, width=%u, %u pixels.\n", _valid? "":"not ", _panelWidth, _len);
DEBUGBUS_PRINTF_P(PSTR("%sstarted, width=%u, %u pixels.\n"), _valid? "":"not ", _panelWidth, _len);
if (_ledBuffer != nullptr) DEBUGBUS_PRINTLN(F("MatrixPanel_I2S_DMA LEDS buffer enabled."));
if (_ledsDirty != nullptr) DEBUGBUS_PRINTLN(F("MatrixPanel_I2S_DMA LEDS dirty bit optimization enabled."));
@@ -1009,8 +1049,8 @@ BusHub75Matrix::BusHub75Matrix(const BusConfig &bc) : Bus(bc.type, bc.start, bc.
}
}
void __attribute__((hot)) BusHub75Matrix::setPixelColor(unsigned pix, uint32_t c) {
if (!_valid || pix >= _len) return;
void IRAM_ATTR BusHub75Matrix::setPixelColor(unsigned pix, uint32_t c) {
if (!_valid) return; // note: no need to check pix >= _len as that is checked in containsPixel()
// if (_cct >= 1900) c = colorBalanceFromKelvin(_cct, c); //color correction from CCT
if (_ledBuffer) {
@@ -1028,8 +1068,8 @@ void __attribute__((hot)) BusHub75Matrix::setPixelColor(unsigned pix, uint32_t c
uint8_t g = G(c);
uint8_t b = B(c);
if(virtualDisp != nullptr) {
int x = pix % _panelWidth;
if (virtualDisp != nullptr) {
int x = pix % _panelWidth; // TODO: check if using & and shift would be faster here, it limits to power-of-2 widths though
int y = pix / _panelWidth;
virtualDisp->drawPixelRGB888(int16_t(x), int16_t(y), r, g, b);
} else {
@@ -1041,41 +1081,36 @@ void __attribute__((hot)) BusHub75Matrix::setPixelColor(unsigned pix, uint32_t c
}
uint32_t BusHub75Matrix::getPixelColor(unsigned pix) const {
if (!_valid || pix >= _len) return IS_BLACK;
if (!_valid) return IS_BLACK; // note: no need to check pix >= _len as that is checked in containsPixel()
if (_ledBuffer)
return uint32_t(_ledBuffer[pix].scale8(_bri)) & 0x00FFFFFF; // scale8() is needed to mimic NeoPixelBus, which returns scaled-down colours
return uint32_t(_ledBuffer[pix]);
else
return getBitFromArray(_ledsDirty, pix) ? IS_DARKGREY: IS_BLACK; // just a hack - we only know if the pixel is black or not
}
void BusHub75Matrix::setBrightness(uint8_t b) {
_bri = b;
if (display) display->setBrightness(_bri);
if (!_valid || !display) return;
display->setBrightness(_bri);
}
void BusHub75Matrix::show(void) {
if (!_valid) return;
display->setBrightness(_bri);
if (_ledBuffer) {
// write out buffered LEDs
bool isVirtualDisp = (virtualDisp != nullptr);
unsigned height = isVirtualDisp ? virtualDisp->height() : display->height();
unsigned height = _isVirtual ? virtualDisp->height() : display->height();
unsigned width = _panelWidth;
//while(!previousBufferFree) delay(1); // experimental - Wait before we allow any writing to the buffer. Stop flicker.
size_t pix = 0; // running pixel index
for (int y=0; y<height; y++) for (int x=0; x<width; x++) {
if (getBitFromArray(_ledsDirty, pix) == true) { // only repaint the "dirty" pixels
uint32_t c = uint32_t(_ledBuffer[pix]) & 0x00FFFFFF; // get RGB color, removing FastLED "alpha" component
uint8_t r = R(c);
uint8_t g = G(c);
uint8_t b = B(c);
if (isVirtualDisp) virtualDisp->drawPixelRGB888(int16_t(x), int16_t(y), r, g, b);
else display->drawPixelRGB888(int16_t(x), int16_t(y), r, g, b);
CRGB c = _ledBuffer[pix];
//c.nscale8_video(_bri); // apply brightness
if (_isVirtual) virtualDisp->drawPixelRGB888(int16_t(x), int16_t(y), c.r, c.g, c.b);
else display->drawPixelRGB888(int16_t(x), int16_t(y), c.r, c.g, c.b);
}
pix ++;
pix++;
}
setBitArray(_ledsDirty, _len, false); // buffer shown - reset all dirty bits
}
@@ -1084,16 +1119,18 @@ void BusHub75Matrix::show(void) {
void BusHub75Matrix::cleanup() {
if (display && _valid) display->stopDMAoutput(); // terminate DMA driver (display goes black)
_valid = false;
delay(30); // give some time to finish DMA
_panelWidth = 0;
deallocatePins();
DEBUGBUS_PRINTLN("HUB75 output ended.");
//if (virtualDisp != nullptr) delete virtualDisp; // warning: deleting object of polymorphic class type 'VirtualMatrixPanel' which has non-virtual destructor might cause undefined behavior
delete display;
DEBUGBUS_PRINTLN(F("HUB75 output ended."));
#ifndef CONFIG_IDF_TARGET_ESP32S3 // on ESP32-S3 deleting display/virtualDisp does not work and leads to crash (DMA issues), request reboot from user instead
if (virtualDisp != nullptr) delete virtualDisp; // note: in MM there is a warning to not do this but if using "NO_GFX" this is safe
if (display != nullptr) delete display;
display = nullptr;
virtualDisp = nullptr;
if (_ledBuffer != nullptr) free(_ledBuffer); _ledBuffer = nullptr;
if (_ledsDirty != nullptr) free(_ledsDirty); _ledsDirty = nullptr;
virtualDisp = nullptr; // note: when not using "NO_GFX" this causes a memory leak
#endif
if (_ledBuffer != nullptr) d_free(_ledBuffer); _ledBuffer = nullptr;
if (_ledsDirty != nullptr) d_free(_ledsDirty); _ledsDirty = nullptr;
}
void BusHub75Matrix::deallocatePins() {
@@ -1114,57 +1151,53 @@ size_t BusHub75Matrix::getPins(uint8_t* pinArray) const {
pinArray[0] = mxconfig.mx_width;
pinArray[1] = mxconfig.mx_height;
pinArray[2] = mxconfig.chain_length;
pinArray[3] = _rows;
pinArray[4] = _cols;
}
return 3;
return 5;
}
#endif
// ***************************************************************************
//utility to get the approx. memory usage of a given BusConfig
size_t BusConfig::memUsage(unsigned nr) const {
BusPlaceholder::BusPlaceholder(const BusConfig &bc)
: Bus(bc.type, bc.start, bc.autoWhite, bc.count, bc.reversed, bc.refreshReq)
, _colorOrder(bc.colorOrder)
, _skipAmount(bc.skipAmount)
, _driverType(bc.driverType)
, _frequency(bc.frequency)
, _milliAmpsPerLed(bc.milliAmpsPerLed)
, _milliAmpsMax(bc.milliAmpsMax)
, _text(bc.text)
{
memcpy(_pins, bc.pins, sizeof(_pins));
}
size_t BusPlaceholder::getPins(uint8_t* pinArray) const {
size_t nPins = Bus::getNumberOfPins(_type);
if (pinArray) {
for (size_t i = 0; i < nPins; i++) pinArray[i] = _pins[i];
}
return nPins;
}
//utility to get the approx. memory usage of a given BusConfig inclduding segmentbuffer and global buffer (4 bytes per pixel)
size_t BusConfig::memUsage() const {
size_t mem = (count + skipAmount) * 8; // 8 bytes per pixel for segment + global buffer
if (Bus::isVirtual(type)) {
return sizeof(BusNetwork) + (count * Bus::getNumberOfChannels(type));
mem += sizeof(BusNetwork) + (count * Bus::getNumberOfChannels(type)); // note: getNumberOfChannels() includes CCT channel if applicable but virtual buses do not use CCT channel buffer
} else if (Bus::isDigital(type)) {
// if any of digital buses uses I2S, there is additional common I2S DMA buffer not accounted for here
return sizeof(BusDigital) + PolyBus::memUsage(count + skipAmount, PolyBus::getI(type, pins, nr));
mem += sizeof(BusDigital) + PolyBus::memUsage(count + skipAmount, iType);
} else if (Bus::isOnOff(type)) {
return sizeof(BusOnOff);
mem += sizeof(BusOnOff);
} else {
return sizeof(BusPwm);
mem += sizeof(BusPwm);
}
return mem;
}
size_t BusManager::memUsage() {
// when ESP32, S2 & S3 use parallel I2S only the largest bus determines the total memory requirements for back buffers
// front buffers are always allocated per bus
unsigned size = 0;
unsigned maxI2S = 0;
#if !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(ESP8266)
unsigned digitalCount = 0;
#endif
for (const auto &bus : busses) {
size += bus->getBusSize();
#if !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(ESP8266)
if (bus->isDigital() && !bus->is2Pin()) {
digitalCount++;
if ((PolyBus::isParallelI2S1Output() && digitalCount <= 8) || (!PolyBus::isParallelI2S1Output() && digitalCount == 1)) {
#ifdef NPB_CONF_4STEP_CADENCE
constexpr unsigned stepFactor = 4; // 4 step cadence (4 bits per pixel bit)
#else
constexpr unsigned stepFactor = 3; // 3 step cadence (3 bits per pixel bit)
#endif
unsigned i2sCommonSize = stepFactor * bus->getLength() * bus->getNumberOfChannels() * (bus->is16bit()+1);
if (i2sCommonSize > maxI2S) maxI2S = i2sCommonSize;
}
}
#endif
}
return size + maxI2S;
}
int BusManager::add(const BusConfig &bc) {
int BusManager::add(const BusConfig &bc, bool placeholder) {
DEBUGBUS_PRINTF_P(PSTR("Bus: Adding bus (p:%d v:%d)\n"), getNumBusses(), getNumVirtualBusses());
unsigned digital = 0;
unsigned analog = 0;
@@ -1174,15 +1207,19 @@ int BusManager::add(const BusConfig &bc) {
if (bus->isDigital() && !bus->is2Pin()) digital++;
if (bus->is2Pin()) twoPin++;
}
if (digital > WLED_MAX_DIGITAL_CHANNELS || analog > WLED_MAX_ANALOG_CHANNELS) return -1;
if (Bus::isVirtual(bc.type)) {
digital += (Bus::isDigital(bc.type) && !Bus::is2Pin(bc.type));
analog += (Bus::isPWM(bc.type) ? Bus::numPWMPins(bc.type) : 0);
if (digital > WLED_MAX_DIGITAL_CHANNELS || analog > WLED_MAX_ANALOG_CHANNELS) placeholder = true; // TODO: add errorFlag here
if (placeholder) {
busses.push_back(make_unique<BusPlaceholder>(bc));
} else if (Bus::isVirtual(bc.type)) {
busses.push_back(make_unique<BusNetwork>(bc));
#ifdef WLED_ENABLE_HUB75MATRIX
} else if (Bus::isHub75(bc.type)) {
busses.push_back(make_unique<BusHub75Matrix>(bc));
#endif
} else if (Bus::isDigital(bc.type)) {
busses.push_back(make_unique<BusDigital>(bc, Bus::is2Pin(bc.type) ? twoPin : digital));
busses.push_back(make_unique<BusDigital>(bc));
} else if (Bus::isOnOff(bc.type)) {
busses.push_back(make_unique<BusOnOff>(bc));
} else {
@@ -1220,49 +1257,35 @@ String BusManager::getLEDTypesJSONString() {
return json;
}
void BusManager::useParallelOutput() {
DEBUGBUS_PRINTLN(F("Bus: Enabling parallel I2S."));
PolyBus::setParallelI2S1Output();
uint8_t BusManager::getI(uint8_t busType, const uint8_t* pins, uint8_t driverPreference) {
return PolyBus::getI(busType, pins, driverPreference);
}
bool BusManager::hasParallelOutput() {
return PolyBus::isParallelI2S1Output();
}
//do not call this method from system context (network callback)
void BusManager::removeAll() {
DEBUGBUS_PRINTLN(F("Removing all."));
//prevents crashes due to deleting busses while in use.
while (!canAllShow()) yield();
busses.clear();
PolyBus::setParallelI2S1Output(false);
#ifndef ESP8266
// Reset channel tracking for fresh allocation
PolyBus::resetChannelTracking();
#endif
}
#ifdef ESP32_DATA_IDLE_HIGH
// #2478
// If enabled, RMT idle level is set to HIGH when off
// to prevent leakage current when using an N-channel MOSFET to toggle LED power
// since I2S outputs are known only during config of buses, lets just assume RMT is used for digital buses
// unused RMT channels should have no effect
void BusManager::esp32RMTInvertIdle() {
bool idle_out;
unsigned rmt = 0;
unsigned u = 0;
for (auto &bus : busses) {
if (bus->getLength()==0 || !bus->isDigital() || bus->is2Pin()) continue;
#if defined(CONFIG_IDF_TARGET_ESP32C3) // 2 RMT, only has 1 I2S but NPB does not support it ATM
if (u > 1) return;
rmt = u;
#elif defined(CONFIG_IDF_TARGET_ESP32S2) // 4 RMT, only has 1 I2S bus, supported in NPB
if (u > 3) return;
rmt = u;
#elif defined(CONFIG_IDF_TARGET_ESP32S3) // 4 RMT, has 2 I2S but NPB does not support them ATM
if (u > 3) return;
rmt = u;
#else
unsigned numI2S = !PolyBus::isParallelI2S1Output(); // if using parallel I2S, RMT is used 1st
if (numI2S > u) continue;
if (u > 7 + numI2S) return;
rmt = u - numI2S;
#endif
if (static_cast<BusDigital*>(bus.get())->isI2S()) continue;
if (u >= WLED_MAX_RMT_CHANNELS) return;
//assumes that bus number to rmt channel mapping stays 1:1
rmt_channel_t ch = static_cast<rmt_channel_t>(rmt);
rmt_idle_level_t lvl;
@@ -1282,7 +1305,7 @@ void BusManager::on() {
if (PinManager::getPinOwner(LED_BUILTIN) == PinOwner::BusDigital) {
for (auto &bus : busses) {
uint8_t pins[2] = {255,255};
if (bus->isDigital() && bus->getPins(pins)) {
if (bus->isDigital() && bus->getPins(pins) && bus->isOk()) {
if (pins[0] == LED_BUILTIN || pins[1] == LED_BUILTIN) {
BusDigital &b = static_cast<BusDigital&>(*bus);
b.begin();
@@ -1292,12 +1315,17 @@ void BusManager::on() {
}
}
#else
static uint32_t nextResolve = 0; // initial resolve is done on bus creation
bool resolveNow = (millis() - nextResolve >= 600000); // wait at least 10 minutes between hostname resolutions (blocking call)
for (auto &bus : busses) if (bus->isVirtual()) {
// virtual/network bus should check for IP change if hostname is specified
// otherwise there are no endpoints to force DNS resolution
BusNetwork &b = static_cast<BusNetwork&>(*bus);
b.resolveHostname();
if (resolveNow)
b.resolveHostname();
}
if (resolveNow)
nextResolve = millis();
#endif
#ifdef ESP32_DATA_IDLE_HIGH
esp32RMTInvertIdle();
@@ -1377,7 +1405,7 @@ void BusManager::initializeABL() {
_useABL = true; // at least one bus has ABL set
uint32_t ESPshare = MA_FOR_ESP / numABLbuses; // share of ESP current per ABL bus
for (auto &bus : busses) {
if (bus->isDigital()) {
if (bus->isDigital() && bus->isOk()) {
BusDigital &busd = static_cast<BusDigital&>(*bus);
uint32_t busLength = busd.getLength();
uint32_t busDemand = busLength * busd.getLEDCurrent();
@@ -1437,12 +1465,18 @@ void BusManager::applyABL() {
ColorOrderMap& BusManager::getColorOrderMap() { return _colorOrderMap; }
#ifndef ESP8266
// PolyBus channel tracking for dynamic allocation
bool PolyBus::_useParallelI2S = false;
uint8_t PolyBus::_rmtChannelsAssigned = 0; // number of RMT channels assigned durig getI() check
uint8_t PolyBus::_rmtChannel = 0; // number of RMT channels actually used during bus creation in create()
uint8_t PolyBus::_i2sChannelsAssigned = 0; // number of I2S channels assigned durig getI() check
uint8_t PolyBus::_parallelBusItype = 0; // type I_NONE
uint8_t PolyBus::_2PchannelsAssigned = 0;
#endif
// Bus static member definition
int16_t Bus::_cct = -1;
uint8_t Bus::_cctBlend = 0; // 0 - 127
int16_t Bus::_cct = -1; // -1 means use approximateKelvinFromRGB(), 0-255 is standard, >1900 use colorBalanceFromKelvin()
int8_t Bus::_cctBlend = 0; // -128 to +127
uint8_t Bus::_gAWM = 255;
uint16_t BusDigital::_milliAmpsTotal = 0;
+76 -27
View File
@@ -6,7 +6,7 @@
#include <ESP32-HUB75-MatrixPanel-I2S-DMA.h>
#include <ESP32-VirtualMatrixPanel-I2S-DMA.h>
#include <FastLED.h>
#include "src/dependencies/fastled_slim/fastled_slim.h"
#endif
/*
@@ -17,6 +17,9 @@
#include "pin_manager.h"
#include <vector>
#include <memory>
#ifdef ARDUINO_ARCH_ESP32
#include "asyncDNS.h"
#endif
#if __cplusplus >= 201402L
using std::make_unique;
@@ -133,14 +136,15 @@ class Bus {
virtual void setColorOrder(uint8_t co) {}
virtual uint32_t getPixelColor(unsigned pix) const { return 0; }
virtual size_t getPins(uint8_t* pinArray = nullptr) const { return 0; }
virtual uint16_t getLength() const { return isOk() ? _len : 0; }
virtual uint16_t getLength() const { return _len; }
virtual uint8_t getColorOrder() const { return COL_ORDER_RGB; }
virtual unsigned skippedLeds() const { return 0; }
virtual uint16_t getFrequency() const { return 0U; }
virtual uint16_t getLEDCurrent() const { return 0; }
virtual uint16_t getUsedCurrent() const { return 0; }
virtual uint16_t getMaxCurrent() const { return 0; }
virtual size_t getBusSize() const { return sizeof(Bus); }
virtual uint8_t getDriverType() const { return 0; } // Default to RMT (0) for non-digital buses
virtual size_t getBusSize() const { return sizeof(Bus); } // currently unused
virtual const String getCustomText() const { return String(); }
inline bool hasRGB() const { return _hasRgb; }
@@ -152,6 +156,7 @@ class Bus {
inline bool isPWM() const { return isPWM(_type); }
inline bool isVirtual() const { return isVirtual(_type); }
inline bool is16bit() const { return is16bit(_type); }
virtual bool isPlaceholder() const { return false; }
inline bool mustRefresh() const { return mustRefresh(_type); }
inline void setReversed(bool reversed) { _reversed = reversed; }
inline void setStart(uint16_t start) { _start = start; }
@@ -166,7 +171,7 @@ class Bus {
inline bool containsPixel(uint16_t pix) const { return pix >= _start && pix < _start + _len; }
static inline std::vector<LEDType> getLEDTypes() { return {{TYPE_NONE, "", PSTR("None")}}; } // not used. just for reference for derived classes
static constexpr size_t getNumberOfPins(uint8_t type) { return isVirtual(type) ? 4 : isPWM(type) ? numPWMPins(type) : isHub75(type) ? 3 : is2Pin(type) + 1; } // credit @PaoloTK
static constexpr size_t getNumberOfPins(uint8_t type) { return isVirtual(type) ? 4 : isPWM(type) ? numPWMPins(type) : isHub75(type) ? 5 : is2Pin(type) + 1; } // credit @PaoloTK; for HUB75 the 5 slots store config params (panelW, panelH, chain, rows, cols), not GPIO pins
static constexpr size_t getNumberOfChannels(uint8_t type) { return hasWhite(type) + 3*hasRGB(type) + hasCCT(type); }
static constexpr bool hasRGB(uint8_t type) {
return !((type >= TYPE_WS2812_1CH && type <= TYPE_WS2812_WWA) || type == TYPE_ANALOG_1CH || type == TYPE_ANALOG_2CH || type == TYPE_ONOFF);
@@ -179,10 +184,9 @@ class Bus {
type == TYPE_NET_DDP_RGBW || type == TYPE_NET_ARTNET_RGBW; // network types with white channel
}
static constexpr bool hasCCT(uint8_t type) {
return type == TYPE_WS2812_2CH_X3 || type == TYPE_WS2812_WWA ||
return type == TYPE_WS2812_WWA || type == TYPE_SM16825 ||
type == TYPE_ANALOG_2CH || type == TYPE_ANALOG_5CH ||
type == TYPE_FW1906 || type == TYPE_WS2805 ||
type == TYPE_SM16825;
type == TYPE_FW1906 || type == TYPE_WS2805;
}
static constexpr bool isTypeValid(uint8_t type) { return (type > 15 && type < 128); }
static constexpr bool isDigital(uint8_t type) { return (type >= TYPE_DIGITAL_MIN && type <= TYPE_DIGITAL_MAX) || is2Pin(type); }
@@ -199,9 +203,9 @@ class Bus {
static inline void setGlobalAWMode(uint8_t m) { if (m < 5) _gAWM = m; else _gAWM = AW_GLOBAL_DISABLED; }
static inline uint8_t getGlobalAWMode() { return _gAWM; }
static inline void setCCT(int16_t cct) { _cct = cct; }
static inline uint8_t getCCTBlend() { return (_cctBlend * 100 + 64) / 127; } // returns 0-100, 100% = 127. +64 for rounding
static inline void setCCTBlend(uint8_t b) { // input is 0-100
_cctBlend = (std::min((int)b,100) * 127 + 50) / 100; // +50 for rounding, b=100% -> 127
static inline int8_t getCCTBlend() { return (_cctBlend * 100 + (_cctBlend >= 0 ? 64 : -64)) / 127; } // returns -100 to +100, +/-100% = +/-127. +/-64 for rounding
static inline void setCCTBlend(int8_t b) { // input is -100 to +100
_cctBlend = (std::max(-100, std::min(100, (int)b)) * 127 + (b >= 0 ? 50 : -50)) / 100; // +/-50 for rounding, b=+/-100% -> +/-127
//compile-time limiter for hardware that can't power both white channels at max
#ifdef WLED_MAX_CCT_BLEND
if (_cctBlend > WLED_MAX_CCT_BLEND) _cctBlend = WLED_MAX_CCT_BLEND;
@@ -230,19 +234,20 @@ class Bus {
// [0,255] is the exact CCT value where 0 means warm and 255 cold
// [1900,10060] only for color correction expressed in K (colorBalanceFromKelvin())
static int16_t _cct;
// _cctBlend determines WW/CW blending:
// _cctBlend determines WW/CW blending, see calculateCCT()
// < 0 - linear blending in center, single white at both ends, single white zone extends with decreased value (-127 min)
// 0 - linear (CCT 127 => 50% warm, 50% cold)
// 63 - semi additive/nonlinear (CCT 127 => 66% warm, 66% cold)
// 127 - additive CCT blending (CCT 127 => 100% warm, 100% cold)
static uint8_t _cctBlend;
static int8_t _cctBlend;
uint32_t autoWhiteCalc(uint32_t c) const;
uint32_t autoWhiteCalc(uint32_t c, uint8_t &ww, uint8_t &cw) const;
};
class BusDigital : public Bus {
public:
BusDigital(const BusConfig &bc, uint8_t nr);
BusDigital(const BusConfig &bc);
~BusDigital() { cleanup(); }
void show() override;
@@ -258,10 +263,12 @@ class BusDigital : public Bus {
uint16_t getLEDCurrent() const override { return _milliAmpsPerLed; }
uint16_t getUsedCurrent() const override { return _milliAmpsTotal; }
uint16_t getMaxCurrent() const override { return _milliAmpsMax; }
uint8_t getDriverType() const override { return _driverType; }
void setCurrentLimit(uint16_t milliAmps) { _milliAmpsLimit = milliAmps; }
void estimateCurrent(); // estimate used current from summed colors
void applyBriLimit(uint8_t newBri);
size_t getBusSize() const override;
bool isI2S(); // true if this bus uses I2S driver
void begin() override;
void cleanup();
@@ -272,6 +279,7 @@ class BusDigital : public Bus {
uint8_t _colorOrder;
uint8_t _pins[2];
uint8_t _iType;
uint8_t _driverType; // 0=RMT (default), 1=I2S
uint16_t _frequencykHz;
uint16_t _milliAmpsMax;
uint8_t _milliAmpsPerLed;
@@ -372,6 +380,41 @@ class BusNetwork : public Bus {
#endif
};
// Placeholder for buses that we can't construct due to resource limitations
// This preserves the configuration so it can be read back to the settings pages
// Function calls "mimic" the replaced bus, isPlaceholder() can be used to identify a placeholder
class BusPlaceholder : public Bus {
public:
BusPlaceholder(const BusConfig &bc);
// Actual calls are stubbed out
void setPixelColor(unsigned pix, uint32_t c) override {};
void show() override {};
// Accessors
uint8_t getColorOrder() const override { return _colorOrder; }
size_t getPins(uint8_t* pinArray) const override;
unsigned skippedLeds() const override { return _skipAmount; }
uint16_t getFrequency() const override { return _frequency; }
uint16_t getLEDCurrent() const override { return _milliAmpsPerLed; }
uint16_t getMaxCurrent() const override { return _milliAmpsMax; }
uint8_t getDriverType() const override { return _driverType; }
const String getCustomText() const override { return _text; }
bool isPlaceholder() const override { return true; }
size_t getBusSize() const override { return sizeof(BusPlaceholder); }
private:
uint8_t _colorOrder;
uint8_t _skipAmount;
uint8_t _pins[OUTPUT_MAX_PINS];
uint8_t _driverType;
uint16_t _frequency;
uint8_t _milliAmpsPerLed;
uint16_t _milliAmpsMax;
String _text;
};
#ifdef WLED_ENABLE_HUB75MATRIX
class BusHub75Matrix : public Bus {
public:
@@ -395,12 +438,15 @@ class BusHub75Matrix : public Bus {
VirtualMatrixPanel *virtualDisp = nullptr;
HUB75_I2S_CFG mxconfig;
unsigned _panelWidth = 0;
CRGB *_ledBuffer = nullptr;
uint8_t _rows = 1; // panels per row
uint8_t _cols = 1; // panels per column
bool _isVirtual = false; // note: this is not strictly needed but there are padding bytes here anyway
CRGB *_ledBuffer = nullptr; // note: using uint32_t buffer is only 2% faster and not worth the extra RAM
byte *_ledsDirty = nullptr;
// workaround for missing constants on include path for non-MM
uint32_t IS_BLACK = 0x000000;
uint32_t IS_DARKGREY = 0x333333;
const int PIN_COUNT = 14;
static constexpr uint32_t IS_BLACK = 0x000000u;
static constexpr uint32_t IS_DARKGREY = 0x333333u;
static constexpr int PIN_COUNT = 14;
};
#endif
@@ -418,9 +464,11 @@ struct BusConfig {
uint16_t frequency;
uint8_t milliAmpsPerLed;
uint16_t milliAmpsMax;
uint8_t driverType; // 0=RMT (default), 1=I2S
uint8_t iType; // internal bus type (I_*) determined during memory estimation, used for bus creation
String text;
BusConfig(uint8_t busType, uint8_t* ppins, uint16_t pstart, uint16_t len = 1, uint8_t pcolorOrder = COL_ORDER_GRB, bool rev = false, uint8_t skip = 0, byte aw=RGBW_MODE_MANUAL_ONLY, uint16_t clock_kHz=0U, uint8_t maPerLed=LED_MILLIAMPS_DEFAULT, uint16_t maMax=ABL_MILLIAMPS_DEFAULT, String sometext = "")
BusConfig(uint8_t busType, uint8_t* ppins, uint16_t pstart, uint16_t len = 1, uint8_t pcolorOrder = COL_ORDER_GRB, bool rev = false, uint8_t skip = 0, byte aw=RGBW_MODE_MANUAL_ONLY, uint16_t clock_kHz=0U, uint8_t maPerLed=LED_MILLIAMPS_DEFAULT, uint16_t maMax=ABL_MILLIAMPS_DEFAULT, uint8_t driver=0, String sometext = "")
: count(std::max(len,(uint16_t)1))
, start(pstart)
, colorOrder(pcolorOrder)
@@ -430,13 +478,15 @@ struct BusConfig {
, frequency(clock_kHz)
, milliAmpsPerLed(maPerLed)
, milliAmpsMax(maMax)
, driverType(driver)
, iType(0) // default to I_NONE
, text(sometext)
{
refreshReq = (bool) GET_BIT(busType,7);
type = busType & 0x7F; // bit 7 may be/is hacked to include refresh info (1=refresh in off state, 0=no refresh)
size_t nPins = Bus::getNumberOfPins(type);
for (size_t i = 0; i < nPins; i++) pins[i] = ppins[i];
DEBUGBUS_PRINTF_P(PSTR("Bus: Config (%d-%d, type:%d, CO:%d, rev:%d, skip:%d, AW:%d kHz:%d, mA:%d/%d)\n"),
DEBUGBUS_PRINTF_P(PSTR("Bus: Config (%d-%d, type:%d, CO:%d, rev:%d, skip:%d, AW:%d kHz:%d, mA:%d/%d, driver:%s)\n"),
(int)start, (int)(start+len),
(int)type,
(int)colorOrder,
@@ -444,14 +494,15 @@ struct BusConfig {
(int)skipAmount,
(int)autoWhite,
(int)frequency,
(int)milliAmpsPerLed, (int)milliAmpsMax
(int)milliAmpsPerLed, (int)milliAmpsMax,
driverType == 0 ? "RMT" : "I2S"
);
}
//validates start and length and extends total if needed
bool adjustBounds(uint16_t& total) {
if (!count) count = 1;
if (count > MAX_LEDS_PER_BUS) count = MAX_LEDS_PER_BUS;
if (!Bus::isVirtual(type) && count > MAX_LEDS_PER_BUS) count = MAX_LEDS_PER_BUS;
if (start >= MAX_LEDS) return false;
//limit length of strip if it would exceed total permissible LEDs
if (start + count > MAX_LEDS) count = MAX_LEDS - start;
@@ -460,7 +511,7 @@ struct BusConfig {
return true;
}
size_t memUsage(unsigned nr = 0) const;
size_t memUsage() const;
};
@@ -491,7 +542,6 @@ namespace BusManager {
return j;
}
size_t memUsage();
inline uint16_t currentMilliamps() { return _gMilliAmpsUsed + MA_FOR_ESP; }
//inline uint16_t ablMilliampsMax() { unsigned sum = 0; for (auto &bus : busses) sum += bus->getMaxCurrent(); return sum; }
inline uint16_t ablMilliampsMax() { return _gMilliAmpsMax; } // used for compatibility reasons (and enabling virtual global ABL)
@@ -499,12 +549,11 @@ namespace BusManager {
void initializeABL(); // setup automatic brightness limiter parameters, call once after buses are initialized
void applyABL(); // apply automatic brightness limiter, global or per bus
void useParallelOutput(); // workaround for inaccessible PolyBus
bool hasParallelOutput(); // workaround for inaccessible PolyBus
uint8_t getI(uint8_t busType, const uint8_t* pins, uint8_t driverPreference); // workaround for access to PolyBus function from FX_fcn.cpp
//do not call this method from system context (network callback)
void removeAll();
int add(const BusConfig &bc);
int add(const BusConfig &bc, bool placeholder);
void on();
void off();
+130 -137
View File
@@ -1,4 +1,4 @@
#pragma once
#pragma once
#ifndef BusWrapper_h
#define BusWrapper_h
@@ -50,7 +50,7 @@
#define I_8266_U1_UCS_4 26
#define I_8266_DM_UCS_4 27
#define I_8266_BB_UCS_4 28
//FW1906 GRBCW
//FW1906 GRBCCT
#define I_8266_U0_FW6_5 29
#define I_8266_U1_FW6_5 30
#define I_8266_DM_FW6_5 31
@@ -60,7 +60,7 @@
#define I_8266_U1_APA106_3 34
#define I_8266_DM_APA106_3 35
#define I_8266_BB_APA106_3 36
//WS2805 (RGBCW)
//WS2805 (RGBCCT)
#define I_8266_U0_2805_5 37
#define I_8266_U1_2805_5 38
#define I_8266_DM_2805_5 39
@@ -70,7 +70,7 @@
#define I_8266_U1_TM1914_3 42
#define I_8266_DM_TM1914_3 43
#define I_8266_BB_TM1914_3 44
//SM16825 (RGBCW)
//SM16825 (RGBCCT)
#define I_8266_U0_SM16825_5 45
#define I_8266_U1_SM16825_5 46
#define I_8266_DM_SM16825_5 47
@@ -98,19 +98,19 @@
//UCS8904 (RGBW)
#define I_32_RN_UCS_4 25
#define I_32_I2_UCS_4 26
//FW1906 GRBCW
//FW1906 GRBCCT 6 color channels
#define I_32_RN_FW6_5 29
#define I_32_I2_FW6_5 30
//APA106
#define I_32_RN_APA106_3 33
#define I_32_I2_APA106_3 34
//WS2805 (RGBCW)
//WS2805 (RGBCCT)
#define I_32_RN_2805_5 37
#define I_32_I2_2805_5 38
//TM1914 (RGB)
#define I_32_RN_TM1914_3 41
#define I_32_I2_TM1914_3 42
//SM16825 (RGBCW)
//SM16825 (RGBCCT)
#define I_32_RN_SM16825_5 45
#define I_32_I2_SM16825_5 46
@@ -180,12 +180,12 @@
#define B_8266_U1_APA106_3 NeoPixelBus<NeoRbgFeature, NeoEsp8266Uart1Apa106Method> //3 chan, esp8266, gpio2
#define B_8266_DM_APA106_3 NeoPixelBus<NeoGrbFeature, NeoEsp8266DmaApa106Method> //3 chan, esp8266, gpio3
#define B_8266_BB_APA106_3 NeoPixelBus<NeoGrbFeature, NeoEsp8266BitBangApa106Method> //3 chan, esp8266, bb (any pin but 16)
//FW1906 GRBCW
//FW1906 GRBCCT
#define B_8266_U0_FW6_5 NeoPixelBus<NeoGrbcwxFeature, NeoEsp8266Uart0Ws2813Method> //esp8266, gpio1
#define B_8266_U1_FW6_5 NeoPixelBus<NeoGrbcwxFeature, NeoEsp8266Uart1Ws2813Method> //esp8266, gpio2
#define B_8266_DM_FW6_5 NeoPixelBus<NeoGrbcwxFeature, NeoEsp8266Dma800KbpsMethod> //esp8266, gpio3
#define B_8266_BB_FW6_5 NeoPixelBus<NeoGrbcwxFeature, NeoEsp8266BitBang800KbpsMethod> //esp8266, bb
//WS2805 GRBCW
//WS2805 GRBCCT
#define B_8266_U0_2805_5 NeoPixelBus<NeoGrbwwFeature, NeoEsp8266Uart0Ws2805Method> //esp8266, gpio1
#define B_8266_U1_2805_5 NeoPixelBus<NeoGrbwwFeature, NeoEsp8266Uart1Ws2805Method> //esp8266, gpio2
#define B_8266_DM_2805_5 NeoPixelBus<NeoGrbwwFeature, NeoEsp8266DmaWs2805Method> //esp8266, gpio3
@@ -195,7 +195,7 @@
#define B_8266_U1_TM1914_3 NeoPixelBus<NeoRgbTm1914Feature, NeoEsp8266Uart1Tm1914Method>
#define B_8266_DM_TM1914_3 NeoPixelBus<NeoRgbTm1914Feature, NeoEsp8266DmaTm1914Method>
#define B_8266_BB_TM1914_3 NeoPixelBus<NeoRgbTm1914Feature, NeoEsp8266BitBangTm1914Method>
//Sm16825 (RGBWC)
//Sm16825 (RGBCCT)
#define B_8266_U0_SM16825_5 NeoPixelBus<NeoRgbwcSm16825eFeature, NeoEsp8266Uart0Ws2813Method>
#define B_8266_U1_SM16825_5 NeoPixelBus<NeoRgbwcSm16825eFeature, NeoEsp8266Uart1Ws2813Method>
#define B_8266_DM_SM16825_5 NeoPixelBus<NeoRgbwcSm16825eFeature, NeoEsp8266Dma800KbpsMethod>
@@ -285,11 +285,11 @@
#define B_32_RN_APA106_3 NeoPixelBus<NeoGrbFeature, NeoEsp32RmtMethod(Apa106)>
#define B_32_I2_APA106_3 NeoPixelBus<NeoGrbFeature, X1Apa106Method>
#define B_32_IP_APA106_3 NeoPixelBus<NeoGrbFeature, X8Apa106Method> // parallel I2S
//FW1906 GRBCW
//FW1906 GRBCCT 6 color channels
#define B_32_RN_FW6_5 NeoPixelBus<NeoGrbcwxFeature, NeoEsp32RmtMethod(Ws2812x)>
#define B_32_I2_FW6_5 NeoPixelBus<NeoGrbcwxFeature, X1800KbpsMethod>
#define B_32_IP_FW6_5 NeoPixelBus<NeoGrbcwxFeature, X8800KbpsMethod> // parallel I2S
//WS2805 RGBWC
//WS2805 RGBCCT
#define B_32_RN_2805_5 NeoPixelBus<NeoGrbwwFeature, NeoEsp32RmtMethod(Ws2805)>
#define B_32_I2_2805_5 NeoPixelBus<NeoGrbwwFeature, X1Ws2805Method>
#define B_32_IP_2805_5 NeoPixelBus<NeoGrbwwFeature, X8Ws2805Method> // parallel I2S
@@ -297,7 +297,7 @@
#define B_32_RN_TM1914_3 NeoPixelBus<NeoGrbTm1914Feature, NeoEsp32RmtMethod(Tm1914)>
#define B_32_I2_TM1914_3 NeoPixelBus<NeoGrbTm1914Feature, X1Tm1914Method>
#define B_32_IP_TM1914_3 NeoPixelBus<NeoGrbTm1914Feature, X8Tm1914Method> // parallel I2S
//Sm16825 (RGBWC)
//Sm16825 (RGBCCT)
#define B_32_RN_SM16825_5 NeoPixelBus<NeoRgbcwSm16825eFeature, NeoEsp32RmtMethod(Ws2812x)>
#define B_32_I2_SM16825_5 NeoPixelBus<NeoRgbcwSm16825eFeature, X1Ws2812xMethod>
#define B_32_IP_SM16825_5 NeoPixelBus<NeoRgbcwSm16825eFeature, X8Ws2812xMethod> // parallel I2S
@@ -339,11 +339,17 @@
//handles pointer type conversion for all possible bus types
class PolyBus {
private:
static bool _useParallelI2S;
#ifndef ESP8266
static bool _useParallelI2S; // use parallel I2S/LCD (8 channels)
static uint8_t _rmtChannelsAssigned; // RMT channel tracking for dynamic allocation
static uint8_t _rmtChannel; // physical RMT channel to use during bus creation
static uint8_t _i2sChannelsAssigned; // I2S channel tracking for dynamic allocation
static uint8_t _parallelBusItype; // parallel output does not allow mixed LED types, track I_Type
static uint8_t _2PchannelsAssigned; // 2-Pin (SPI) channel assigned: first one gets the hardware SPI, others use bit-banged SPI
// note on 2-Pin Types: all supported types except WS2801 use start/stop or latch frames, speed is not critical. WS2801 uses a 500us timeout and is prone to flickering if bit-banged too slow.
// TODO: according to #4863 using more than one bit-banged output can cause glitches even in APA102. This needs investigation as from a hardware perspective all but WS2801 should be immune to timing issues.
#endif
public:
static inline void setParallelI2S1Output(bool b = true) { _useParallelI2S = b; }
static inline bool isParallelI2S1Output(void) { return _useParallelI2S; }
// initialize SPI bus speed for DotStar methods
template <class T>
@@ -352,6 +358,7 @@ class PolyBus {
#ifdef ESP8266
dotStar_strip->Begin();
#else
if (miso == -1) miso = 127; // note: in arduino core, -1 means "default" not "none", passing 127 as the MISO pin is a workaround to prevent SPI.begin() assign the default pin, see #5670
if (sck == -1 && mosi == -1) dotStar_strip->Begin();
else dotStar_strip->Begin(sck, miso, mosi, ss);
#endif
@@ -476,21 +483,7 @@ class PolyBus {
}
}
static void* create(uint8_t busType, uint8_t* pins, uint16_t len, uint8_t channel) {
// NOTE: "channel" is only used on ESP32 (and its variants) for RMT channel allocation
#if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32C3)
if (_useParallelI2S && (channel >= 8)) {
// Parallel I2S channels are to be used first, so subtract 8 to get the RMT channel number
channel -= 8;
}
#endif
#if defined(ARDUINO_ARCH_ESP32) && !(defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3))
// since 0.15.0-b3 I2S1 is favoured for classic ESP32 and moved to position 0 (channel 0) so we need to subtract 1 for correct RMT allocation
if (!_useParallelI2S && channel > 0) channel--; // accommodate I2S1 which is used as 1st bus on classic ESP32
#endif
static void* create(uint8_t busType, uint8_t* pins, uint16_t len) {
void* busPtr = nullptr;
switch (busType) {
case I_NONE: break;
@@ -546,18 +539,18 @@ class PolyBus {
#endif
#ifdef ARDUINO_ARCH_ESP32
// RMT buses
case I_32_RN_NEO_3: busPtr = new B_32_RN_NEO_3(len, pins[0], (NeoBusChannel)channel); break;
case I_32_RN_NEO_4: busPtr = new B_32_RN_NEO_4(len, pins[0], (NeoBusChannel)channel); break;
case I_32_RN_400_3: busPtr = new B_32_RN_400_3(len, pins[0], (NeoBusChannel)channel); break;
case I_32_RN_TM1_4: busPtr = new B_32_RN_TM1_4(len, pins[0], (NeoBusChannel)channel); break;
case I_32_RN_TM2_3: busPtr = new B_32_RN_TM2_3(len, pins[0], (NeoBusChannel)channel); break;
case I_32_RN_UCS_3: busPtr = new B_32_RN_UCS_3(len, pins[0], (NeoBusChannel)channel); break;
case I_32_RN_UCS_4: busPtr = new B_32_RN_UCS_4(len, pins[0], (NeoBusChannel)channel); break;
case I_32_RN_APA106_3: busPtr = new B_32_RN_APA106_3(len, pins[0], (NeoBusChannel)channel); break;
case I_32_RN_FW6_5: busPtr = new B_32_RN_FW6_5(len, pins[0], (NeoBusChannel)channel); break;
case I_32_RN_2805_5: busPtr = new B_32_RN_2805_5(len, pins[0], (NeoBusChannel)channel); break;
case I_32_RN_TM1914_3: busPtr = new B_32_RN_TM1914_3(len, pins[0], (NeoBusChannel)channel); break;
case I_32_RN_SM16825_5: busPtr = new B_32_RN_SM16825_5(len, pins[0], (NeoBusChannel)channel); break;
case I_32_RN_NEO_3: busPtr = new B_32_RN_NEO_3(len, pins[0], (NeoBusChannel)_rmtChannel++); break;
case I_32_RN_NEO_4: busPtr = new B_32_RN_NEO_4(len, pins[0], (NeoBusChannel)_rmtChannel++); break;
case I_32_RN_400_3: busPtr = new B_32_RN_400_3(len, pins[0], (NeoBusChannel)_rmtChannel++); break;
case I_32_RN_TM1_4: busPtr = new B_32_RN_TM1_4(len, pins[0], (NeoBusChannel)_rmtChannel++); break;
case I_32_RN_TM2_3: busPtr = new B_32_RN_TM2_3(len, pins[0], (NeoBusChannel)_rmtChannel++); break;
case I_32_RN_UCS_3: busPtr = new B_32_RN_UCS_3(len, pins[0], (NeoBusChannel)_rmtChannel++); break;
case I_32_RN_UCS_4: busPtr = new B_32_RN_UCS_4(len, pins[0], (NeoBusChannel)_rmtChannel++); break;
case I_32_RN_APA106_3: busPtr = new B_32_RN_APA106_3(len, pins[0], (NeoBusChannel)_rmtChannel++); break;
case I_32_RN_FW6_5: busPtr = new B_32_RN_FW6_5(len, pins[0], (NeoBusChannel)_rmtChannel++); break;
case I_32_RN_2805_5: busPtr = new B_32_RN_2805_5(len, pins[0], (NeoBusChannel)_rmtChannel++); break;
case I_32_RN_TM1914_3: busPtr = new B_32_RN_TM1914_3(len, pins[0], (NeoBusChannel)_rmtChannel++); break;
case I_32_RN_SM16825_5: busPtr = new B_32_RN_SM16825_5(len, pins[0], (NeoBusChannel)_rmtChannel++); break;
// I2S1 bus or paralell buses
#ifndef CONFIG_IDF_TARGET_ESP32C3
case I_32_I2_NEO_3: if (_useParallelI2S) busPtr = new B_32_IP_NEO_3(len, pins[0]); else busPtr = new B_32_I2_NEO_3(len, pins[0]); break;
@@ -1118,6 +1111,9 @@ class PolyBus {
static unsigned getDataSize(void* busPtr, uint8_t busType) {
unsigned size = 0;
#ifdef ARDUINO_ARCH_ESP32
size = 100; // ~100bytes for NPB internal structures (measured for both I2S and RMT, much smaller and more variable on ESP8266)
#endif
switch (busType) {
case I_NONE: break;
#ifdef ESP8266
@@ -1172,32 +1168,32 @@ class PolyBus {
#endif
#ifdef ARDUINO_ARCH_ESP32
// RMT buses (front + back + small system managed RMT)
case I_32_RN_NEO_3: size = (static_cast<B_32_RN_NEO_3*>(busPtr))->PixelsSize()*2; break;
case I_32_RN_NEO_4: size = (static_cast<B_32_RN_NEO_4*>(busPtr))->PixelsSize()*2; break;
case I_32_RN_400_3: size = (static_cast<B_32_RN_400_3*>(busPtr))->PixelsSize()*2; break;
case I_32_RN_TM1_4: size = (static_cast<B_32_RN_TM1_4*>(busPtr))->PixelsSize()*2; break;
case I_32_RN_TM2_3: size = (static_cast<B_32_RN_TM2_3*>(busPtr))->PixelsSize()*2; break;
case I_32_RN_UCS_3: size = (static_cast<B_32_RN_UCS_3*>(busPtr))->PixelsSize()*2; break;
case I_32_RN_UCS_4: size = (static_cast<B_32_RN_UCS_4*>(busPtr))->PixelsSize()*2; break;
case I_32_RN_APA106_3: size = (static_cast<B_32_RN_APA106_3*>(busPtr))->PixelsSize()*2; break;
case I_32_RN_FW6_5: size = (static_cast<B_32_RN_FW6_5*>(busPtr))->PixelsSize()*2; break;
case I_32_RN_2805_5: size = (static_cast<B_32_RN_2805_5*>(busPtr))->PixelsSize()*2; break;
case I_32_RN_TM1914_3: size = (static_cast<B_32_RN_TM1914_3*>(busPtr))->PixelsSize()*2; break;
case I_32_RN_SM16825_5: size = (static_cast<B_32_RN_SM16825_5*>(busPtr))->PixelsSize()*2; break;
// I2S1 bus or paralell buses (front + DMA; DMA = front * cadence, aligned to 4 bytes)
case I_32_RN_NEO_3: size += (static_cast<B_32_RN_NEO_3*>(busPtr))->PixelsSize()*2; break;
case I_32_RN_NEO_4: size += (static_cast<B_32_RN_NEO_4*>(busPtr))->PixelsSize()*2; break;
case I_32_RN_400_3: size += (static_cast<B_32_RN_400_3*>(busPtr))->PixelsSize()*2; break;
case I_32_RN_TM1_4: size += (static_cast<B_32_RN_TM1_4*>(busPtr))->PixelsSize()*2; break;
case I_32_RN_TM2_3: size += (static_cast<B_32_RN_TM2_3*>(busPtr))->PixelsSize()*2; break;
case I_32_RN_UCS_3: size += (static_cast<B_32_RN_UCS_3*>(busPtr))->PixelsSize()*2; break;
case I_32_RN_UCS_4: size += (static_cast<B_32_RN_UCS_4*>(busPtr))->PixelsSize()*2; break;
case I_32_RN_APA106_3: size += (static_cast<B_32_RN_APA106_3*>(busPtr))->PixelsSize()*2; break;
case I_32_RN_FW6_5: size += (static_cast<B_32_RN_FW6_5*>(busPtr))->PixelsSize()*2; break;
case I_32_RN_2805_5: size += (static_cast<B_32_RN_2805_5*>(busPtr))->PixelsSize()*2; break;
case I_32_RN_TM1914_3: size += (static_cast<B_32_RN_TM1914_3*>(busPtr))->PixelsSize()*2; break;
case I_32_RN_SM16825_5: size += (static_cast<B_32_RN_SM16825_5*>(busPtr))->PixelsSize()*2; break;
// I2S1 bus or paralell buses (front + DMA; DMA = front * cadence, aligned to 4 bytes) not: for parallel I2S only the largest bus counts for DMA memory, this is not done correctly here, also assumes 3-step cadence
#ifndef CONFIG_IDF_TARGET_ESP32C3
case I_32_I2_NEO_3: size = (_useParallelI2S) ? (static_cast<B_32_IP_NEO_3*>(busPtr))->PixelsSize()*4 : (static_cast<B_32_I2_NEO_3*>(busPtr))->PixelsSize()*4; break;
case I_32_I2_NEO_4: size = (_useParallelI2S) ? (static_cast<B_32_IP_NEO_4*>(busPtr))->PixelsSize()*4 : (static_cast<B_32_I2_NEO_4*>(busPtr))->PixelsSize()*4; break;
case I_32_I2_400_3: size = (_useParallelI2S) ? (static_cast<B_32_IP_400_3*>(busPtr))->PixelsSize()*4 : (static_cast<B_32_I2_400_3*>(busPtr))->PixelsSize()*4; break;
case I_32_I2_TM1_4: size = (_useParallelI2S) ? (static_cast<B_32_IP_TM1_4*>(busPtr))->PixelsSize()*4 : (static_cast<B_32_I2_TM1_4*>(busPtr))->PixelsSize()*4; break;
case I_32_I2_TM2_3: size = (_useParallelI2S) ? (static_cast<B_32_IP_TM2_3*>(busPtr))->PixelsSize()*4 : (static_cast<B_32_I2_TM2_3*>(busPtr))->PixelsSize()*4; break;
case I_32_I2_UCS_3: size = (_useParallelI2S) ? (static_cast<B_32_IP_UCS_3*>(busPtr))->PixelsSize()*4 : (static_cast<B_32_I2_UCS_3*>(busPtr))->PixelsSize()*4; break;
case I_32_I2_UCS_4: size = (_useParallelI2S) ? (static_cast<B_32_IP_UCS_4*>(busPtr))->PixelsSize()*4 : (static_cast<B_32_I2_UCS_4*>(busPtr))->PixelsSize()*4; break;
case I_32_I2_APA106_3: size = (_useParallelI2S) ? (static_cast<B_32_IP_APA106_3*>(busPtr))->PixelsSize()*4 : (static_cast<B_32_I2_APA106_3*>(busPtr))->PixelsSize()*4; break;
case I_32_I2_FW6_5: size = (_useParallelI2S) ? (static_cast<B_32_IP_FW6_5*>(busPtr))->PixelsSize()*4 : (static_cast<B_32_I2_FW6_5*>(busPtr))->PixelsSize()*4; break;
case I_32_I2_2805_5: size = (_useParallelI2S) ? (static_cast<B_32_IP_2805_5*>(busPtr))->PixelsSize()*4 : (static_cast<B_32_I2_2805_5*>(busPtr))->PixelsSize()*4; break;
case I_32_I2_TM1914_3: size = (_useParallelI2S) ? (static_cast<B_32_IP_TM1914_3*>(busPtr))->PixelsSize()*4 : (static_cast<B_32_I2_TM1914_3*>(busPtr))->PixelsSize()*4; break;
case I_32_I2_SM16825_5: size = (_useParallelI2S) ? (static_cast<B_32_IP_SM16825_5*>(busPtr))->PixelsSize()*4 : (static_cast<B_32_I2_SM16825_5*>(busPtr))->PixelsSize()*4; break;
case I_32_I2_NEO_3: size += (_useParallelI2S) ? (static_cast<B_32_IP_NEO_3*>(busPtr))->PixelsSize()*4 : (static_cast<B_32_I2_NEO_3*>(busPtr))->PixelsSize()*4; break;
case I_32_I2_NEO_4: size += (_useParallelI2S) ? (static_cast<B_32_IP_NEO_4*>(busPtr))->PixelsSize()*4 : (static_cast<B_32_I2_NEO_4*>(busPtr))->PixelsSize()*4; break;
case I_32_I2_400_3: size += (_useParallelI2S) ? (static_cast<B_32_IP_400_3*>(busPtr))->PixelsSize()*4 : (static_cast<B_32_I2_400_3*>(busPtr))->PixelsSize()*4; break;
case I_32_I2_TM1_4: size += (_useParallelI2S) ? (static_cast<B_32_IP_TM1_4*>(busPtr))->PixelsSize()*4 : (static_cast<B_32_I2_TM1_4*>(busPtr))->PixelsSize()*4; break;
case I_32_I2_TM2_3: size += (_useParallelI2S) ? (static_cast<B_32_IP_TM2_3*>(busPtr))->PixelsSize()*4 : (static_cast<B_32_I2_TM2_3*>(busPtr))->PixelsSize()*4; break;
case I_32_I2_UCS_3: size += (_useParallelI2S) ? (static_cast<B_32_IP_UCS_3*>(busPtr))->PixelsSize()*4 : (static_cast<B_32_I2_UCS_3*>(busPtr))->PixelsSize()*4; break;
case I_32_I2_UCS_4: size += (_useParallelI2S) ? (static_cast<B_32_IP_UCS_4*>(busPtr))->PixelsSize()*4 : (static_cast<B_32_I2_UCS_4*>(busPtr))->PixelsSize()*4; break;
case I_32_I2_APA106_3: size += (_useParallelI2S) ? (static_cast<B_32_IP_APA106_3*>(busPtr))->PixelsSize()*4 : (static_cast<B_32_I2_APA106_3*>(busPtr))->PixelsSize()*4; break;
case I_32_I2_FW6_5: size += (_useParallelI2S) ? (static_cast<B_32_IP_FW6_5*>(busPtr))->PixelsSize()*4 : (static_cast<B_32_I2_FW6_5*>(busPtr))->PixelsSize()*4; break;
case I_32_I2_2805_5: size += (_useParallelI2S) ? (static_cast<B_32_IP_2805_5*>(busPtr))->PixelsSize()*4 : (static_cast<B_32_I2_2805_5*>(busPtr))->PixelsSize()*4; break;
case I_32_I2_TM1914_3: size += (_useParallelI2S) ? (static_cast<B_32_IP_TM1914_3*>(busPtr))->PixelsSize()*4 : (static_cast<B_32_I2_TM1914_3*>(busPtr))->PixelsSize()*4; break;
case I_32_I2_SM16825_5: size += (_useParallelI2S) ? (static_cast<B_32_IP_SM16825_5*>(busPtr))->PixelsSize()*4 : (static_cast<B_32_I2_SM16825_5*>(busPtr))->PixelsSize()*4; break;
#endif
#endif
case I_HS_DOT_3: size = (static_cast<B_HS_DOT_3*>(busPtr))->PixelsSize()*2; break;
@@ -1255,6 +1251,7 @@ class PolyBus {
case I_8266_DM_2805_5 : size = (size + 2*count)*5; break;
case I_8266_DM_SM16825_5: size = (size + 2*count)*2*5; break;
#else
// note: RMT and I2S buses use ~100 bytes of internal NPB memory each, not included here for simplicity
// RMT buses (1x front and 1x back buffer, does not include small RMT buffer)
case I_32_RN_NEO_4 : // fallthrough
case I_32_RN_TM1_4 : size = (size + count)*2; break; // 4 channels
@@ -1263,7 +1260,7 @@ class PolyBus {
case I_32_RN_FW6_5 : // fallthrough
case I_32_RN_2805_5 : size = (size + 2*count)*2; break; // 5 channels
case I_32_RN_SM16825_5: size = (size + 2*count)*2*2; break; // 16bit, 5 channels
// I2S1 bus or paralell I2S1 buses (1x front, does not include DMA buffer which is front*cadence, a bit(?) more for LCD)
// I2S bus or paralell I2S buses (1x front, does not include DMA buffer which is front*cadence, a bit(?) more for LCD)
#ifndef CONFIG_IDF_TARGET_ESP32C3
case I_32_I2_NEO_3 : // fallthrough
case I_32_I2_400_3 : // fallthrough
@@ -1282,129 +1279,125 @@ class PolyBus {
}
return size;
}
//gives back the internal type index (I_XX_XXX_X above) for the input
static uint8_t getI(uint8_t busType, const uint8_t* pins, uint8_t num = 0) {
#ifndef ESP8266
// Reset channel tracking (call before adding buses)
static void resetChannelTracking() {
_useParallelI2S = false;
_rmtChannelsAssigned = 0;
_rmtChannel = 0;
_i2sChannelsAssigned = 0;
_parallelBusItype = I_NONE;
_2PchannelsAssigned = 0;
}
#endif
// reserves and gives back the internal type index (I_XX_XXX_X above) for the input based on bus type and pins
static uint8_t getI(uint8_t busType, const uint8_t* pins, uint8_t driverPreference) {
if (!Bus::isDigital(busType)) return I_NONE;
uint8_t t = I_NONE;
if (Bus::is2Pin(busType)) { //SPI LED chips
bool isHSPI = false;
#ifdef ESP8266
if (pins[0] == P_8266_HS_MOSI && pins[1] == P_8266_HS_CLK) isHSPI = true;
#else
// temporary hack to limit use of hardware SPI to a single SPI peripheral (HSPI): only allow ESP32 hardware serial on segment 0
// SPI global variable is normally linked to VSPI on ESP32 (or FSPI C3, S3)
if (!num) isHSPI = true;
if (_2PchannelsAssigned == 0) isHSPI = true; // first 2-pin channel uses hardware SPI
_2PchannelsAssigned++;
#endif
uint8_t t = I_NONE;
switch (busType) {
case TYPE_APA102: t = I_SS_DOT_3; break;
case TYPE_LPD8806: t = I_SS_LPD_3; break;
case TYPE_LPD6803: t = I_SS_LPO_3; break;
case TYPE_WS2801: t = I_SS_WS1_3; break;
case TYPE_P9813: t = I_SS_P98_3; break;
default: t=I_NONE;
}
if (t > I_NONE && isHSPI) t--; //hardware SPI has one smaller ID than software
return t;
} else {
#ifdef ESP8266
uint8_t offset = pins[0] -1; //for driver: 0 = uart0, 1 = uart1, 2 = dma, 3 = bitbang
if (offset > 3) offset = 3;
switch (busType) {
case TYPE_WS2812_1CH_X3:
case TYPE_WS2812_2CH_X3:
case TYPE_WS2812_RGB:
case TYPE_WS2812_WWA:
return I_8266_U0_NEO_3 + offset;
t = I_8266_U0_NEO_3 + offset; break;
case TYPE_SK6812_RGBW:
return I_8266_U0_NEO_4 + offset;
t = I_8266_U0_NEO_4 + offset; break;
case TYPE_WS2811_400KHZ:
return I_8266_U0_400_3 + offset;
t = I_8266_U0_400_3 + offset; break;
case TYPE_TM1814:
return I_8266_U0_TM1_4 + offset;
t = I_8266_U0_TM1_4 + offset; break;
case TYPE_TM1829:
return I_8266_U0_TM2_3 + offset;
t = I_8266_U0_TM2_3 + offset; break;
case TYPE_UCS8903:
return I_8266_U0_UCS_3 + offset;
t = I_8266_U0_UCS_3 + offset; break;
case TYPE_UCS8904:
return I_8266_U0_UCS_4 + offset;
t = I_8266_U0_UCS_4 + offset; break;
case TYPE_APA106:
return I_8266_U0_APA106_3 + offset;
t = I_8266_U0_APA106_3 + offset; break;
case TYPE_FW1906:
return I_8266_U0_FW6_5 + offset;
t = I_8266_U0_FW6_5 + offset; break;
case TYPE_WS2805:
return I_8266_U0_2805_5 + offset;
t = I_8266_U0_2805_5 + offset; break;
case TYPE_TM1914:
return I_8266_U0_TM1914_3 + offset;
t = I_8266_U0_TM1914_3 + offset; break;
case TYPE_SM16825:
return I_8266_U0_SM16825_5 + offset;
t = I_8266_U0_SM16825_5 + offset; break;
}
#else //ESP32
uint8_t offset = 0; // 0 = RMT (num 1-8), 1 = I2S1 [I2S0 is used by Audioreactive]
#if defined(CONFIG_IDF_TARGET_ESP32S2)
// ESP32-S2 only has 4 RMT channels
if (_useParallelI2S) {
if (num > 11) return I_NONE;
if (num < 8) offset = 1; // use x8 parallel I2S0 channels followed by RMT
// Note: conflicts with AudioReactive if enabled
// dynamic channel allocation based on driver preference
// determine which driver to use based on preference and availability. First I2S bus locks the I2S type, all subsequent I2S buses are assigned the same type (hardware restriction)
uint8_t offset = 0; // 0 = RMT, 1 = I2S/LCD
if (driverPreference == 0 && _rmtChannelsAssigned < WLED_MAX_RMT_CHANNELS) {
_rmtChannelsAssigned++;
} else if (_i2sChannelsAssigned < WLED_MAX_I2S_CHANNELS) {
offset = 1; // I2S requested or RMT full
_i2sChannelsAssigned++;
} else {
if (num > 4) return I_NONE;
if (num > 3) offset = 1; // only one I2S0 (use last to allow Audioreactive)
return I_NONE; // No channels available
}
#elif defined(CONFIG_IDF_TARGET_ESP32C3)
// On ESP32-C3 only the first 2 RMT channels are usable for transmitting
if (num > 1) return I_NONE;
//if (num > 1) offset = 1; // I2S not supported yet (only 1 I2S)
#elif defined(CONFIG_IDF_TARGET_ESP32S3)
// On ESP32-S3 only the first 4 RMT channels are usable for transmitting
if (_useParallelI2S) {
if (num > 11) return I_NONE;
if (num < 8) offset = 1; // use x8 parallel I2S LCD channels, followed by RMT
} else {
if (num > 3) return I_NONE; // do not use single I2S (as it is not supported)
}
#else
// standard ESP32 has 8 RMT and x1/x8 I2S1 channels
if (_useParallelI2S) {
if (num > 15) return I_NONE;
if (num < 8) offset = 1; // 8 I2S followed by 8 RMT
} else {
if (num > 9) return I_NONE;
if (num == 0) offset = 1; // prefer I2S1 for 1st bus (less flickering but more RAM needed)
}
#endif
// Now determine actual bus type with the chosen offset
switch (busType) {
case TYPE_WS2812_1CH_X3:
case TYPE_WS2812_2CH_X3:
case TYPE_WS2812_RGB:
case TYPE_WS2812_WWA:
return I_32_RN_NEO_3 + offset;
t = I_32_RN_NEO_3 + offset; break;
case TYPE_SK6812_RGBW:
return I_32_RN_NEO_4 + offset;
t = I_32_RN_NEO_4 + offset; break;
case TYPE_WS2811_400KHZ:
return I_32_RN_400_3 + offset;
t = I_32_RN_400_3 + offset; break;
case TYPE_TM1814:
return I_32_RN_TM1_4 + offset;
t = I_32_RN_TM1_4 + offset; break;
case TYPE_TM1829:
return I_32_RN_TM2_3 + offset;
t = I_32_RN_TM2_3 + offset; break;
case TYPE_UCS8903:
return I_32_RN_UCS_3 + offset;
t = I_32_RN_UCS_3 + offset; break;
case TYPE_UCS8904:
return I_32_RN_UCS_4 + offset;
t = I_32_RN_UCS_4 + offset; break;
case TYPE_APA106:
return I_32_RN_APA106_3 + offset;
t = I_32_RN_APA106_3 + offset; break;
case TYPE_FW1906:
return I_32_RN_FW6_5 + offset;
t = I_32_RN_FW6_5 + offset; break;
case TYPE_WS2805:
return I_32_RN_2805_5 + offset;
t = I_32_RN_2805_5 + offset; break;
case TYPE_TM1914:
return I_32_RN_TM1914_3 + offset;
t = I_32_RN_TM1914_3 + offset; break;
case TYPE_SM16825:
return I_32_RN_SM16825_5 + offset;
t = I_32_RN_SM16825_5 + offset; break;
}
// If using parallel I2S, set the type accordingly
if (_i2sChannelsAssigned == 1 && offset == 1) { // first I2S channel request, lock the type
_parallelBusItype = t;
#ifdef CONFIG_IDF_TARGET_ESP32S3
_useParallelI2S = true; // ESP32-S3 always uses parallel I2S (LCD method)
#endif
}
else if (offset == 1) { // not first I2S channel, use locked type and enable parallel flag
_useParallelI2S = true;
t = _parallelBusItype;
}
#endif
}
return I_NONE;
return t;
}
};
#endif
+82 -78
View File
@@ -17,13 +17,13 @@ static bool buttonBriDirection = false; // true: increase brightness, false: dec
void shortPressAction(uint8_t b)
{
if (!macroButton[b]) {
if (!buttons[b].macroButton) {
switch (b) {
case 0: toggleOnOff(); stateUpdated(CALL_MODE_BUTTON); break;
case 1: ++effectCurrent %= strip.getModeCount(); stateChanged = true; colorUpdated(CALL_MODE_BUTTON); break;
}
} else {
applyPreset(macroButton[b], CALL_MODE_BUTTON_PRESET);
applyPreset(buttons[b].macroButton, CALL_MODE_BUTTON_PRESET);
}
#ifndef WLED_DISABLE_MQTT
@@ -38,7 +38,7 @@ void shortPressAction(uint8_t b)
void longPressAction(uint8_t b)
{
if (!macroLongPress[b]) {
if (!buttons[b].macroLongPress) {
switch (b) {
case 0: setRandomColor(colPri); colorUpdated(CALL_MODE_BUTTON); break;
case 1:
@@ -52,11 +52,11 @@ void longPressAction(uint8_t b)
else bri -= WLED_LONG_BRI_STEPS;
}
stateUpdated(CALL_MODE_BUTTON);
buttonPressedTime[b] = millis();
buttons[b].pressedTime = millis();
break; // repeatable action
}
} else {
applyPreset(macroLongPress[b], CALL_MODE_BUTTON_PRESET);
applyPreset(buttons[b].macroLongPress, CALL_MODE_BUTTON_PRESET);
}
#ifndef WLED_DISABLE_MQTT
@@ -71,13 +71,13 @@ void longPressAction(uint8_t b)
void doublePressAction(uint8_t b)
{
if (!macroDoublePress[b]) {
if (!buttons[b].macroDoublePress) {
switch (b) {
//case 0: toggleOnOff(); colorUpdated(CALL_MODE_BUTTON); break; //instant short press on button 0 if no macro set
case 1: ++effectPalette %= getPaletteCount(); colorUpdated(CALL_MODE_BUTTON); break;
}
} else {
applyPreset(macroDoublePress[b], CALL_MODE_BUTTON_PRESET);
applyPreset(buttons[b].macroDoublePress, CALL_MODE_BUTTON_PRESET);
}
#ifndef WLED_DISABLE_MQTT
@@ -92,10 +92,10 @@ void doublePressAction(uint8_t b)
bool isButtonPressed(uint8_t b)
{
if (btnPin[b]<0) return false;
unsigned pin = btnPin[b];
if (buttons[b].pin < 0) return false;
unsigned pin = buttons[b].pin;
switch (buttonType[b]) {
switch (buttons[b].type) {
case BTN_TYPE_NONE:
case BTN_TYPE_RESERVED:
break;
@@ -113,7 +113,7 @@ bool isButtonPressed(uint8_t b)
#ifdef SOC_TOUCH_VERSION_2 //ESP32 S2 and S3 provide a function to check touch state (state is updated in interrupt)
if (touchInterruptGetLastStatus(pin)) return true;
#else
if (digitalPinToTouchChannel(btnPin[b]) >= 0 && touchRead(pin) <= touchThreshold) return true;
if (digitalPinToTouchChannel(pin) >= 0 && touchRead(pin) <= touchThreshold) return true;
#endif
#endif
break;
@@ -124,25 +124,25 @@ bool isButtonPressed(uint8_t b)
void handleSwitch(uint8_t b)
{
// isButtonPressed() handles inverted/noninverted logic
if (buttonPressedBefore[b] != isButtonPressed(b)) {
if (buttons[b].pressedBefore != isButtonPressed(b)) {
DEBUG_PRINTF_P(PSTR("Switch: State changed %u\n"), b);
buttonPressedTime[b] = millis();
buttonPressedBefore[b] = !buttonPressedBefore[b];
buttons[b].pressedTime = millis();
buttons[b].pressedBefore = !buttons[b].pressedBefore; // toggle pressed state
}
if (buttonLongPressed[b] == buttonPressedBefore[b]) return;
if (buttons[b].longPressed == buttons[b].pressedBefore) return;
if (millis() - buttonPressedTime[b] > WLED_DEBOUNCE_THRESHOLD) { //fire edge event only after 50ms without change (debounce)
if (millis() - buttons[b].pressedTime > WLED_DEBOUNCE_THRESHOLD) { //fire edge event only after 50ms without change (debounce)
DEBUG_PRINTF_P(PSTR("Switch: Activating %u\n"), b);
if (!buttonPressedBefore[b]) { // on -> off
if (!buttons[b].pressedBefore) { // on -> off
DEBUG_PRINTF_P(PSTR("Switch: On -> Off (%u)\n"), b);
if (macroButton[b]) applyPreset(macroButton[b], CALL_MODE_BUTTON_PRESET);
if (buttons[b].macroButton) applyPreset(buttons[b].macroButton, CALL_MODE_BUTTON_PRESET);
else { //turn on
if (!bri) {toggleOnOff(); stateUpdated(CALL_MODE_BUTTON);}
}
} else { // off -> on
DEBUG_PRINTF_P(PSTR("Switch: Off -> On (%u)\n"), b);
if (macroLongPress[b]) applyPreset(macroLongPress[b], CALL_MODE_BUTTON_PRESET);
if (buttons[b].macroLongPress) applyPreset(buttons[b].macroLongPress, CALL_MODE_BUTTON_PRESET);
else { //turn off
if (bri) {toggleOnOff(); stateUpdated(CALL_MODE_BUTTON);}
}
@@ -152,13 +152,13 @@ void handleSwitch(uint8_t b)
// publish MQTT message
if (buttonPublishMqtt && WLED_MQTT_CONNECTED) {
char subuf[MQTT_MAX_TOPIC_LEN + 32];
if (buttonType[b] == BTN_TYPE_PIR_SENSOR) sprintf_P(subuf, PSTR("%s/motion/%d"), mqttDeviceTopic, (int)b);
if (buttons[b].type == BTN_TYPE_PIR_SENSOR) sprintf_P(subuf, PSTR("%s/motion/%d"), mqttDeviceTopic, (int)b);
else sprintf_P(subuf, _mqtt_topic_button, mqttDeviceTopic, (int)b);
mqtt->publish(subuf, 0, false, !buttonPressedBefore[b] ? "off" : "on");
mqtt->publish(subuf, 0, false, !buttons[b].pressedBefore ? "off" : "on");
}
#endif
buttonLongPressed[b] = buttonPressedBefore[b]; //save the last "long term" switch state
buttons[b].longPressed = buttons[b].pressedBefore; //save the last "long term" switch state
}
}
@@ -178,17 +178,17 @@ void handleAnalog(uint8_t b)
#ifdef ESP8266
rawReading = analogRead(A0) << 2; // convert 10bit read to 12bit
#else
if ((btnPin[b] < 0) /*|| (digitalPinToAnalogChannel(btnPin[b]) < 0)*/) return; // pin must support analog ADC - newer esp32 frameworks throw lots of warnings otherwise
rawReading = analogRead(btnPin[b]); // collect at full 12bit resolution
if ((buttons[b].pin < 0) /*|| (digitalPinToAnalogChannel(buttons[b].pin) < 0)*/) return; // pin must support analog ADC - newer esp32 frameworks throw lots of warnings otherwise
rawReading = analogRead(buttons[b].pin); // collect at full 12bit resolution
#endif
yield(); // keep WiFi task running - analog read may take several millis on ESP8266
filteredReading[b] += POT_SMOOTHING * ((float(rawReading) / 16.0f) - filteredReading[b]); // filter raw input, and scale to [0..255]
unsigned aRead = max(min(int(filteredReading[b]), 255), 0); // squash into 8bit
if(aRead <= POT_SENSITIVITY) aRead = 0; // make sure that 0 and 255 are used
if(aRead >= 255-POT_SENSITIVITY) aRead = 255;
if (aRead <= POT_SENSITIVITY) aRead = 0; // make sure that 0 and 255 are used
if (aRead >= 255-POT_SENSITIVITY) aRead = 255;
if (buttonType[b] == BTN_TYPE_ANALOG_INVERTED) aRead = 255 - aRead;
if (buttons[b].type == BTN_TYPE_ANALOG_INVERTED) aRead = 255 - aRead;
// remove noise & reduce frequency of UI updates
if (abs(int(aRead) - int(oldRead[b])) <= POT_SENSITIVITY) return; // no significant change in reading
@@ -206,10 +206,10 @@ void handleAnalog(uint8_t b)
oldRead[b] = aRead;
// if no macro for "short press" and "long press" is defined use brightness control
if (!macroButton[b] && !macroLongPress[b]) {
DEBUG_PRINTF_P(PSTR("Analog: Action = %u\n"), macroDoublePress[b]);
if (!buttons[b].macroButton && !buttons[b].macroLongPress) {
DEBUG_PRINTF_P(PSTR("Analog: Action = %u\n"), buttons[b].macroDoublePress);
// if "double press" macro defines which option to change
if (macroDoublePress[b] >= 250) {
if (buttons[b].macroDoublePress >= 250) {
// global brightness
if (aRead == 0) {
briLast = bri;
@@ -218,27 +218,30 @@ void handleAnalog(uint8_t b)
if (bri == 0) strip.restartRuntime();
bri = aRead;
}
} else if (macroDoublePress[b] == 249) {
} else if (buttons[b].macroDoublePress == 249) {
// effect speed
effectSpeed = aRead;
} else if (macroDoublePress[b] == 248) {
} else if (buttons[b].macroDoublePress == 248) {
// effect intensity
effectIntensity = aRead;
} else if (macroDoublePress[b] == 247) {
} else if (buttons[b].macroDoublePress == 247) {
// selected palette
effectPalette = map(aRead, 0, 252, 0, getPaletteCount()-1);
effectPalette = constrain(effectPalette, 0, getPaletteCount()-1); // map is allowed to "overshoot", so we need to contrain the result
} else if (macroDoublePress[b] == 200) {
} else if (buttons[b].macroDoublePress == 200) {
// primary color, hue, full saturation
colorHStoRGB(aRead*256,255,colPri);
colorHStoRGB(aRead*256, 255, colPri);
} else {
// otherwise use "double press" for segment selection
Segment& seg = strip.getSegment(macroDoublePress[b]);
Segment& seg = strip.getSegment(buttons[b].macroDoublePress);
if (aRead == 0) {
seg.setOption(SEG_OPTION_ON, false); // off (use transition)
seg.on = false; // do not use transition
//seg.setOption(SEG_OPTION_ON, false); // off (use transition)
} else {
seg.setOpacity(aRead);
seg.setOption(SEG_OPTION_ON, true); // on (use transition)
seg.opacity = aRead; // set brightness (opacity) of segment
seg.on = true;
//seg.setOpacity(aRead);
//seg.setOption(SEG_OPTION_ON, true); // on (use transition)
}
// this will notify clients of update (websockets,mqtt,etc)
updateInterfaces(CALL_MODE_BUTTON);
@@ -261,16 +264,16 @@ void handleButton()
if (strip.isUpdating() && (now - lastRun < ANALOG_BTN_READ_CYCLE+1)) return; // don't interfere with strip update (unless strip is updating continuously, e.g. very long strips)
lastRun = now;
for (unsigned b=0; b<WLED_MAX_BUTTONS; b++) {
for (unsigned b = 0; b < buttons.size(); b++) {
#ifdef ESP8266
if ((btnPin[b]<0 && !(buttonType[b] == BTN_TYPE_ANALOG || buttonType[b] == BTN_TYPE_ANALOG_INVERTED)) || buttonType[b] == BTN_TYPE_NONE) continue;
if ((buttons[b].pin < 0 && !(buttons[b].type == BTN_TYPE_ANALOG || buttons[b].type == BTN_TYPE_ANALOG_INVERTED)) || buttons[b].type == BTN_TYPE_NONE) continue;
#else
if (btnPin[b]<0 || buttonType[b] == BTN_TYPE_NONE) continue;
if (buttons[b].pin < 0 || buttons[b].type == BTN_TYPE_NONE) continue;
#endif
if (UsermodManager::handleButton(b)) continue; // did usermod handle buttons
if (buttonType[b] == BTN_TYPE_ANALOG || buttonType[b] == BTN_TYPE_ANALOG_INVERTED) { // button is not a button but a potentiometer
if (buttons[b].type == BTN_TYPE_ANALOG || buttons[b].type == BTN_TYPE_ANALOG_INVERTED) { // button is not a button but a potentiometer
if (now - lastAnalogRead > ANALOG_BTN_READ_CYCLE) {
handleAnalog(b);
}
@@ -278,7 +281,7 @@ void handleButton()
}
// button is not momentary, but switch. This is only suitable on pins whose on-boot state does not matter (NOT gpio0)
if (buttonType[b] == BTN_TYPE_SWITCH || buttonType[b] == BTN_TYPE_TOUCH_SWITCH || buttonType[b] == BTN_TYPE_PIR_SENSOR) {
if (buttons[b].type == BTN_TYPE_SWITCH || buttons[b].type == BTN_TYPE_TOUCH_SWITCH || buttons[b].type == BTN_TYPE_PIR_SENSOR) {
handleSwitch(b);
continue;
}
@@ -287,70 +290,66 @@ void handleButton()
if (isButtonPressed(b)) { // pressed
// if all macros are the same, fire action immediately on rising edge
if (macroButton[b] && macroButton[b] == macroLongPress[b] && macroButton[b] == macroDoublePress[b]) {
if (!buttonPressedBefore[b])
shortPressAction(b);
buttonPressedBefore[b] = true;
buttonPressedTime[b] = now; // continually update (for debouncing to work in release handler)
if (buttons[b].macroButton && buttons[b].macroButton == buttons[b].macroLongPress && buttons[b].macroButton == buttons[b].macroDoublePress) {
if (!buttons[b].pressedBefore) shortPressAction(b);
buttons[b].pressedBefore = true;
buttons[b].pressedTime = now; // continually update (for debouncing to work in release handler)
continue;
}
if (!buttonPressedBefore[b]) buttonPressedTime[b] = now;
buttonPressedBefore[b] = true;
if (!buttons[b].pressedBefore) buttons[b].pressedTime = now;
buttons[b].pressedBefore = true;
if (now - buttonPressedTime[b] > WLED_LONG_PRESS) { //long press
if (!buttonLongPressed[b]) {
if (now - buttons[b].pressedTime > WLED_LONG_PRESS) { //long press
if (!buttons[b].longPressed) {
buttonBriDirection = !buttonBriDirection; //toggle brightness direction on long press
longPressAction(b);
} else if (b) { //repeatable action (~5 times per s) on button > 0
longPressAction(b);
buttonPressedTime[b] = now - WLED_LONG_REPEATED_ACTION; //200ms
buttons[b].pressedTime = now - WLED_LONG_REPEATED_ACTION; //200ms
}
buttonLongPressed[b] = true;
buttons[b].longPressed = true;
}
} else if (buttonPressedBefore[b]) { //released
long dur = now - buttonPressedTime[b];
} else if (buttons[b].pressedBefore) { //released
long dur = now - buttons[b].pressedTime;
// released after rising-edge short press action
if (macroButton[b] && macroButton[b] == macroLongPress[b] && macroButton[b] == macroDoublePress[b]) {
if (dur > WLED_DEBOUNCE_THRESHOLD) buttonPressedBefore[b] = false; // debounce, blocks button for 50 ms once it has been released
if (buttons[b].macroButton && buttons[b].macroButton == buttons[b].macroLongPress && buttons[b].macroButton == buttons[b].macroDoublePress) {
if (dur > WLED_DEBOUNCE_THRESHOLD) buttons[b].pressedBefore = false; // debounce, blocks button for 50 ms once it has been released
continue;
}
if (dur < WLED_DEBOUNCE_THRESHOLD) {buttonPressedBefore[b] = false; continue;} // too short "press", debounce
bool doublePress = buttonWaitTime[b]; //did we have a short press before?
buttonWaitTime[b] = 0;
if (dur < WLED_DEBOUNCE_THRESHOLD) {buttons[b].pressedBefore = false; continue;} // too short "press", debounce
bool doublePress = buttons[b].waitTime; //did we have a short press before?
buttons[b].waitTime = 0;
if (b == 0 && dur > WLED_LONG_AP) { // long press on button 0 (when released)
if (dur > WLED_LONG_FACTORY_RESET) { // factory reset if pressed > 10 seconds
WLED_FS.format();
#ifdef WLED_ADD_EEPROM_SUPPORT
clearEEPROM();
#endif
doReboot = true;
} else {
WLED::instance().initAP(true);
}
} else if (!buttonLongPressed[b]) { //short press
} else if (!buttons[b].longPressed) { //short press
//NOTE: this interferes with double click handling in usermods so usermod needs to implement full button handling
if (b != 1 && !macroDoublePress[b]) { //don't wait for double press on buttons without a default action if no double press macro set
if (b != 1 && !buttons[b].macroDoublePress) { //don't wait for double press on buttons without a default action if no double press macro set
shortPressAction(b);
} else { //double press if less than 350 ms between current press and previous short press release (buttonWaitTime!=0)
if (doublePress) {
doublePressAction(b);
} else {
buttonWaitTime[b] = now;
buttons[b].waitTime = now;
}
}
}
buttonPressedBefore[b] = false;
buttonLongPressed[b] = false;
buttons[b].pressedBefore = false;
buttons[b].longPressed = false;
}
//if 350ms elapsed since last short press release it is a short press
if (buttonWaitTime[b] && now - buttonWaitTime[b] > WLED_DOUBLE_PRESS && !buttonPressedBefore[b]) {
buttonWaitTime[b] = 0;
if (buttons[b].waitTime && now - buttons[b].waitTime > WLED_DOUBLE_PRESS && !buttons[b].pressedBefore) {
buttons[b].waitTime = 0;
shortPressAction(b);
}
}
@@ -368,24 +367,29 @@ void handleIO()
// if we want to control on-board LED (ESP8266) or relay we have to do it here as the final show() may not happen until
// next loop() cycle
if (strip.getBrightness()) {
handleOnOff();
}
void handleOnOff(bool forceOff)
{
if (strip.getBrightness() && !forceOff) {
lastOnTime = millis();
if (offMode) {
BusManager::on();
if (rlyPin>=0) {
pinMode(rlyPin, rlyOpenDrain ? OUTPUT_OPEN_DRAIN : OUTPUT);
digitalWrite(rlyPin, rlyMde);
delay(50); // wait for relay to switch and power to stabilize
// note: pinMode is set in first call to handleOnOff(true) in beginStrip()
digitalWrite(rlyPin, rlyMde); // set to on state
delay(RELAY_DELAY); // let power stabilize before sending LED data (#346 #812 #3581 #3955)
}
offMode = false;
}
} else if (millis() - lastOnTime > 600 && !strip.needsUpdate()) {
} else if ((millis() - lastOnTime > 600 && !strip.needsUpdate()) || forceOff) {
// for turning LED or relay off we need to wait until strip no longer needs updates (strip.trigger())
if (!offMode) {
BusManager::off();
if (rlyPin>=0) {
digitalWrite(rlyPin, !rlyMde); // set output before disabling high-z state to avoid output glitches
pinMode(rlyPin, rlyOpenDrain ? OUTPUT_OPEN_DRAIN : OUTPUT);
digitalWrite(rlyPin, !rlyMde);
}
offMode = true;
}
+156 -150
View File
@@ -30,15 +30,17 @@ static constexpr bool validatePinsAndTypes(const unsigned* types, unsigned numTy
// Pins provided < pins required -> always invalid
// Pins provided = pins required -> always valid
// Pins provided > pins required -> valid if excess pins are a product of last type pins since it will be repeated
return (sumPinsRequired(types, numTypes) > numPins) ? false :
(numPins - sumPinsRequired(types, numTypes)) % Bus::getNumberOfPins(types[numTypes-1]) == 0;
// HUB75 types use their pin slots for config params, not GPIO - skip GPIO pin validation for them
return Bus::isHub75(types[numTypes-1]) ? true :
(sumPinsRequired(types, numTypes) > numPins) ? false :
(numPins - sumPinsRequired(types, numTypes)) % Bus::getNumberOfPins(types[numTypes-1]) == 0;
}
//simple macro for ArduinoJSON's or syntax
#define CJSON(a,b) a = b | a
void getStringFromJson(char* dest, const char* src, size_t len) {
static inline void getStringFromJson(char* dest, const char* src, size_t len) {
if (src != nullptr) strlcpy(dest, src, len);
}
@@ -47,14 +49,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) {
//int rev_major = doc["rev"][0]; // 1
//int rev_minor = doc["rev"][1]; // 0
//long vid = doc[F("vid")]; // 2010020
#ifdef WLED_USE_ETHERNET
JsonObject ethernet = doc[F("eth")];
CJSON(ethernetType, ethernet["type"]);
// NOTE: Ethernet configuration takes priority over other use of pins
initEthernet();
#endif
long vid = doc[F("vid")] | VERSION; // 2605010 note: "vid" can be used to detect an update from older versions but only on first call, it is written to the new VID after buses are initialized
JsonObject id = doc["id"];
getStringFromJson(cmDNS, id[F("mdns")], 33);
@@ -114,6 +109,17 @@ bool deserializeConfig(JsonObject doc, bool fromFS) {
multiWiFi[n].staticIP = nIP;
multiWiFi[n].staticGW = nGW;
multiWiFi[n].staticSN = nSN;
#ifdef WLED_ENABLE_WPA_ENTERPRISE
byte encType = WIFI_ENCRYPTION_TYPE_PSK;
char anonIdent[65] = "";
char ident[65] = "";
CJSON(encType, wifi[F("enc_type")]);
getStringFromJson(anonIdent, wifi["e_anon_ident"], 65);
getStringFromJson(ident, wifi["e_ident"], 65);
multiWiFi[n].encryptionType = encType;
strlcpy(multiWiFi[n].enterpriseAnonIdentity, anonIdent, 65);
strlcpy(multiWiFi[n].enterpriseIdentity, ident, 65);
#endif
if (++n >= WLED_MAX_WIFI_COUNT) break;
}
}
@@ -125,12 +131,20 @@ bool deserializeConfig(JsonObject doc, bool fromFS) {
}
}
// https://github.com/wled/WLED/issues/5247
#ifdef WLED_USE_ETHERNET
JsonObject ethernet = doc[F("eth")];
CJSON(ethernetType, ethernet["type"]);
// NOTE: Ethernet configuration takes priority over other use of pins
initEthernet();
#endif
JsonObject ap = doc["ap"];
getStringFromJson(apSSID, ap[F("ssid")], 33);
getStringFromJson(apPass, ap["psk"] , 65); //normally not present due to security
//int ap_pskl = ap[F("pskl")];
CJSON(apChannel, ap[F("chan")]);
if (apChannel > 13 || apChannel < 1) apChannel = 1;
if (apChannel > 13 || apChannel < 1) apChannel = 6; // reset to default if invalid
CJSON(apHide, ap[F("hide")]);
if (apHide > 1) apHide = 1;
CJSON(apBehavior, ap[F("behav")]);
@@ -164,10 +178,8 @@ bool deserializeConfig(JsonObject doc, bool fromFS) {
CJSON(cctICused, hw_led[F("ic")]);
uint8_t cctBlending = hw_led[F("cb")] | Bus::getCCTBlend();
Bus::setCCTBlend(cctBlending);
strip.setTargetFps(hw_led["fps"]); //NOP if 0, default 42 FPS
#if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32C3)
CJSON(useParallelI2S, hw_led[F("prl")]);
#endif
unsigned targetFPS = hw_led["fps"] | WLED_FPS;
strip.setTargetFps(targetFPS); //unlimited if 0, default 42 FPS
#ifndef WLED_DISABLE_2D
// 2D Matrix Settings
@@ -234,9 +246,10 @@ bool deserializeConfig(JsonObject doc, bool fromFS) {
maMax = 0;
}
ledType |= refresh << 7; // hack bit 7 to indicate strip requires off refresh
uint8_t driverType = elm[F("drv")] | 0; // 0=RMT (default), 1=I2S note: polybus may override this if driver is not available
String host = elm[F("text")] | String();
busConfigs.emplace_back(ledType, pins, start, length, colorOrder, reversed, skipFirst, AWmode, freqkHz, maPerLed, maMax, host);
busConfigs.emplace_back(ledType, pins, start, length, colorOrder, reversed, skipFirst, AWmode, freqkHz, maPerLed, maMax, driverType, host);
doInitBusses = true; // finalization done in beginStrip()
if (!Bus::isVirtual(ledType)) s++; // have as many virtual buses as you want
}
@@ -319,7 +332,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) {
unsigned start = 0;
// analog always has length 1
if (Bus::isPWM(dataType) || Bus::isOnOff(dataType)) count = 1;
busConfigs.emplace_back(dataType, defPin, start, count, DEFAULT_LED_COLOR_ORDER, false, 0, RGBW_MODE_MANUAL_ONLY, 0);
busConfigs.emplace_back(dataType, defPin, start, count, DEFAULT_LED_COLOR_ORDER, false, 0, RGBW_MODE_MANUAL_ONLY, 0, LED_MILLIAMPS_DEFAULT, ABL_MILLIAMPS_DEFAULT, 0); // driver=0 (RMT default)
doInitBusses = true; // finalization done in beginStrip()
}
}
@@ -345,97 +358,91 @@ bool deserializeConfig(JsonObject doc, bool fromFS) {
JsonArray hw_btn_ins = btn_obj["ins"];
if (!hw_btn_ins.isNull()) {
// deallocate existing button pins
for (unsigned b = 0; b < WLED_MAX_BUTTONS; b++) PinManager::deallocatePin(btnPin[b], PinOwner::Button); // does nothing if trying to deallocate a pin with PinOwner != Button
for (const auto &button : buttons) PinManager::deallocatePin(button.pin, PinOwner::Button); // does nothing if trying to deallocate a pin with PinOwner != Button
buttons.clear(); // clear existing buttons
unsigned s = 0;
for (JsonObject btn : hw_btn_ins) {
CJSON(buttonType[s], btn["type"]);
int8_t pin = btn["pin"][0] | -1;
uint8_t type = btn["type"] | BTN_TYPE_NONE;
int8_t pin = btn["pin"][0] | -1;
if (pin > -1 && PinManager::allocatePin(pin, false, PinOwner::Button)) {
btnPin[s] = pin;
#ifdef ARDUINO_ARCH_ESP32
#ifdef ARDUINO_ARCH_ESP32
// ESP32 only: check that analog button pin is a valid ADC gpio
if ((buttonType[s] == BTN_TYPE_ANALOG) || (buttonType[s] == BTN_TYPE_ANALOG_INVERTED)) {
if (digitalPinToAnalogChannel(btnPin[s]) < 0) {
if ((type == BTN_TYPE_ANALOG) || (type == BTN_TYPE_ANALOG_INVERTED)) {
if (digitalPinToAnalogChannel(pin) < 0) {
// not an ADC analog pin
DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for analog button #%d is not an analog pin!\n"), btnPin[s], s);
btnPin[s] = -1;
PinManager::deallocatePin(pin,PinOwner::Button);
DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for analog button #%d is not an analog pin!\n"), pin, s);
PinManager::deallocatePin(pin, PinOwner::Button);
pin = -1;
continue;
} else {
analogReadResolution(12); // see #4040
}
}
else if ((buttonType[s] == BTN_TYPE_TOUCH || buttonType[s] == BTN_TYPE_TOUCH_SWITCH))
{
if (digitalPinToTouchChannel(btnPin[s]) < 0) {
} else if ((type == BTN_TYPE_TOUCH || type == BTN_TYPE_TOUCH_SWITCH)) {
if (digitalPinToTouchChannel(pin) < 0) {
// not a touch pin
DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for touch button #%d is not a touch pin!\n"), btnPin[s], s);
btnPin[s] = -1;
PinManager::deallocatePin(pin,PinOwner::Button);
}
DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for touch button #%d is not a touch pin!\n"), pin, s);
PinManager::deallocatePin(pin, PinOwner::Button);
pin = -1;
continue;
}
//if touch pin, enable the touch interrupt on ESP32 S2 & S3
#ifdef SOC_TOUCH_VERSION_2 // ESP32 S2 and S3 have a function to check touch state but need to attach an interrupt to do so
else
{
touchAttachInterrupt(btnPin[s], touchButtonISR, touchThreshold << 4); // threshold on Touch V2 is much higher (1500 is a value given by Espressif example, I measured changes of over 5000)
}
else touchAttachInterrupt(pin, touchButtonISR, touchThreshold << 4); // threshold on Touch V2 is much higher (1500 is a value given by Espressif example, I measured changes of over 5000)
#endif
}
else
#endif
} else
#endif
{
// regular buttons and switches
if (disablePullUp) {
pinMode(btnPin[s], INPUT);
pinMode(pin, INPUT);
} else {
#ifdef ESP32
pinMode(btnPin[s], buttonType[s]==BTN_TYPE_PUSH_ACT_HIGH ? INPUT_PULLDOWN : INPUT_PULLUP);
pinMode(pin, type==BTN_TYPE_PUSH_ACT_HIGH ? INPUT_PULLDOWN : INPUT_PULLUP);
#else
pinMode(btnPin[s], INPUT_PULLUP);
pinMode(pin, INPUT_PULLUP);
#endif
}
}
} else {
btnPin[s] = -1;
JsonArray hw_btn_ins_0_macros = btn["macros"];
uint8_t press = hw_btn_ins_0_macros[0] | 0;
uint8_t longPress = hw_btn_ins_0_macros[1] | 0;
uint8_t doublePress = hw_btn_ins_0_macros[2] | 0;
buttons.emplace_back(pin, type, press, longPress, doublePress); // add button to vector
}
JsonArray hw_btn_ins_0_macros = btn["macros"];
CJSON(macroButton[s], hw_btn_ins_0_macros[0]);
CJSON(macroLongPress[s],hw_btn_ins_0_macros[1]);
CJSON(macroDoublePress[s], hw_btn_ins_0_macros[2]);
if (++s >= WLED_MAX_BUTTONS) break; // max buttons reached
}
// clear remaining buttons
for (; s<WLED_MAX_BUTTONS; s++) {
btnPin[s] = -1;
buttonType[s] = BTN_TYPE_NONE;
macroButton[s] = 0;
macroLongPress[s] = 0;
macroDoublePress[s] = 0;
}
} else if (fromFS) {
// new install/missing configuration (button 0 has defaults)
// relies upon only being called once with fromFS == true, which is currently true.
for (size_t s = 0; s < WLED_MAX_BUTTONS; s++) {
if (buttonType[s] == BTN_TYPE_NONE || btnPin[s] < 0 || !PinManager::allocatePin(btnPin[s], false, PinOwner::Button)) {
btnPin[s] = -1;
buttonType[s] = BTN_TYPE_NONE;
constexpr uint8_t defTypes[] = {BTNTYPE};
constexpr int8_t defPins[] = {BTNPIN};
constexpr unsigned numTypes = (sizeof(defTypes) / sizeof(defTypes[0]));
constexpr unsigned numPins = (sizeof(defPins) / sizeof(defPins[0]));
// check if the number of pins and types are valid; count of pins must be greater than or equal to types
static_assert(numTypes <= numPins, "The default button pins defined in BTNPIN do not match the button types defined in BTNTYPE");
uint8_t type = BTN_TYPE_NONE;
buttons.clear(); // clear existing buttons (just in case)
for (size_t s = 0; s < WLED_MAX_BUTTONS && s < numPins; s++) {
type = defTypes[s < numTypes ? s : numTypes - 1]; // use last known type to set current type if types less than pins
if (type == BTN_TYPE_NONE || defPins[s] < 0 || !PinManager::allocatePin(defPins[s], false, PinOwner::Button)) {
if (buttons.size() == 0) buttons.emplace_back(-1, BTN_TYPE_NONE); // add disabled button to vector (so we have at least one button defined)
continue; // pin not available or invalid, skip configuring this GPIO
}
if (btnPin[s] >= 0) {
if (disablePullUp) {
pinMode(btnPin[s], INPUT);
} else {
#ifdef ESP32
pinMode(btnPin[s], buttonType[s]==BTN_TYPE_PUSH_ACT_HIGH ? INPUT_PULLDOWN : INPUT_PULLUP);
#else
pinMode(btnPin[s], INPUT_PULLUP);
#endif
}
if (disablePullUp) {
pinMode(defPins[s], INPUT);
} else {
#ifdef ESP32
pinMode(defPins[s], type==BTN_TYPE_PUSH_ACT_HIGH ? INPUT_PULLDOWN : INPUT_PULLUP);
#else
pinMode(defPins[s], INPUT_PULLUP);
#endif
}
macroButton[s] = 0;
macroLongPress[s] = 0;
macroDoublePress[s] = 0;
buttons.emplace_back(defPins[s], type); // add button to vector
}
}
CJSON(buttonPublishMqtt,btn_obj["mqtt"]);
CJSON(buttonPublishMqtt, btn_obj["mqtt"]);
#ifndef WLED_DISABLE_INFRARED
int hw_ir_pin = hw["ir"]["pin"] | -2; // 4
@@ -512,13 +519,13 @@ bool deserializeConfig(JsonObject doc, bool fromFS) {
CJSON(strip.autoSegments, light[F("aseg")]);
CJSON(gammaCorrectVal, light["gc"]["val"]); // default 2.2
float light_gc_bri = light["gc"]["bri"];
float light_gc_col = light["gc"]["col"];
if (light_gc_bri > 1.0f) gammaCorrectBri = true;
else gammaCorrectBri = false;
if (light_gc_col > 1.0f) gammaCorrectCol = true;
else gammaCorrectCol = false;
if (gammaCorrectVal <= 1.0f || gammaCorrectVal > 3) {
float light_gc_bri = light["gc"]["bri"] | 1.0f; // default to 1.0 (false)
float light_gc_col = light["gc"]["col"] | gammaCorrectVal; // default to gammaCorrectVal (true)
if (light_gc_bri != 1.0f) gammaCorrectBri = true;
else gammaCorrectBri = false;
if (light_gc_col != 1.0f) gammaCorrectCol = true;
else gammaCorrectCol = false;
if (gammaCorrectVal < 0.1f || gammaCorrectVal > 3) {
gammaCorrectVal = 1.0f; // no gamma correction
gammaCorrectBri = false;
gammaCorrectCol = false;
@@ -685,37 +692,38 @@ bool deserializeConfig(JsonObject doc, bool fromFS) {
CJSON(macroCountdown, cntdwn["macro"]);
setCountdown();
JsonArray timers = tm["ins"];
uint8_t it = 0;
for (JsonObject timer : timers) {
if (it > 9) break;
if (it<8 && timer[F("hour")]==255) it=8; // hour==255 -> sunrise/sunset
CJSON(timerHours[it], timer[F("hour")]);
CJSON(timerMinutes[it], timer["min"]);
CJSON(timerMacro[it], timer["macro"]);
JsonArray timersArray = tm["ins"];
if (!timersArray.isNull()) {
clearTimers();
bool legacySunriseLoaded = false; // migration flag: pre 16.0 used hour=255 for both sunrise & sunset (type determined by array position)
for (JsonObject timer : timersArray) {
uint8_t h = timer[F("hour")] | 0;
// legacy migration for pre 16.0 (vid < 2605010): first occurrence = sunrise, second occurrence = sunset
if (vid < 2605010 && h == 255) {
if (legacySunriseLoaded) {
h = TH_SUNSET; // second "255" entry is actually sunset
} else {
legacySunriseLoaded = true;
}
}
byte dowPrev = timerWeekday[it];
//note: act is currently only 0 or 1.
//the reason we are not using bool is that the on-disk type in 0.11.0 was already int
int actPrev = timerWeekday[it] & 0x01;
CJSON(timerWeekday[it], timer[F("dow")]);
if (timerWeekday[it] != dowPrev) { //present in JSON
timerWeekday[it] <<= 1; //add active bit
int act = timer["en"] | actPrev;
if (act) timerWeekday[it]++;
int8_t m = timer[F("min")] | 0;
uint8_t p = timer[F("macro")] | 0;
uint8_t dow = timer[F("dow")] | 127;
uint8_t wd = (dow << 1) | ((timer[F("en")] | 0) ? 1 : 0);
uint8_t ms = 1, me = 12, ds = 1, de = 31;
JsonObject start = timer[F("start")];
if (!start.isNull()) {
ms = start[F("mon")] | 1;
ds = start[F("day")] | 1;
}
JsonObject end = timer[F("end")];
if (!end.isNull()) {
me = end[F("mon")] | 12;
de = end[F("day")] | 31;
}
addTimer(p, h, m, wd, ms, me, ds, de);
}
if (it<8) {
JsonObject start = timer["start"];
byte startm = start["mon"];
if (startm) timerMonth[it] = (startm << 4);
CJSON(timerDay[it], start["day"]);
JsonObject end = timer["end"];
CJSON(timerDayEnd[it], end["day"]);
byte endm = end["mon"];
if (startm) timerMonth[it] += endm & 0x0F;
if (!(timerMonth[it] & 0x0F)) timerMonth[it] += 12; //default end month to 12
}
it++;
}
JsonObject ota = doc["ota"];
@@ -795,13 +803,8 @@ void resetConfig() {
bool deserializeConfigFromFS() {
[[maybe_unused]] bool success = deserializeConfigSec();
#ifdef WLED_ADD_EEPROM_SUPPORT
if (!success) { //if file does not exist, try reading from EEPROM
deEEPSettings();
}
#endif
if (!requestJSONBufferLock(1)) return false;
if (!requestJSONBufferLock(JSON_LOCK_CFG_DES)) return false;
DEBUG_PRINTLN(F("Reading settings from /cfg.json..."));
@@ -822,7 +825,7 @@ void serializeConfigToFS() {
DEBUG_PRINTLN(F("Writing settings to /cfg.json..."));
if (!requestJSONBufferLock(2)) return;
if (!requestJSONBufferLock(JSON_LOCK_CFG_SER)) return;
JsonObject root = pDoc->to<JsonObject>();
@@ -876,6 +879,13 @@ void serializeConfig(JsonObject root) {
wifi_gw.add(multiWiFi[n].staticGW[i]);
wifi_sn.add(multiWiFi[n].staticSN[i]);
}
#ifdef WLED_ENABLE_WPA_ENTERPRISE
wifi[F("enc_type")] = multiWiFi[n].encryptionType;
if (multiWiFi[n].encryptionType == WIFI_ENCRYPTION_TYPE_ENTERPRISE) {
wifi[F("e_anon_ident")] = multiWiFi[n].enterpriseAnonIdentity;
wifi[F("e_ident")] = multiWiFi[n].enterpriseIdentity;
}
#endif
}
JsonArray dns = nw.createNestedArray(F("dns"));
@@ -939,9 +949,6 @@ void serializeConfig(JsonObject root) {
hw_led[F("cb")] = Bus::getCCTBlend();
hw_led["fps"] = strip.getTargetFps();
hw_led[F("rgbwm")] = Bus::getGlobalAWMode(); // global auto white mode override
#if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32C3)
hw_led[F("prl")] = BusManager::hasParallelOutput();
#endif
#ifndef WLED_DISABLE_2D
// 2D Matrix Settings
@@ -968,7 +975,7 @@ void serializeConfig(JsonObject root) {
for (size_t s = 0; s < BusManager::getNumBusses(); s++) {
DEBUG_PRINTF_P(PSTR("Cfg: Saving bus #%u\n"), s);
const Bus *bus = BusManager::getBus(s);
if (!bus || !bus->isOk()) break;
if (!bus) break; // Memory corruption, iterator invalid
DEBUG_PRINTF_P(PSTR(" (%d-%d, type:%d, CO:%d, rev:%d, skip:%d, AW:%d kHz:%d, mA:%d/%d)\n"),
(int)bus->getStart(), (int)(bus->getStart()+bus->getLength()),
(int)(bus->getType() & 0x7F),
@@ -995,6 +1002,7 @@ void serializeConfig(JsonObject root) {
ins[F("freq")] = bus->getFrequency();
ins[F("maxpwr")] = bus->getMaxCurrent();
ins[F("ledma")] = bus->getLEDCurrent();
ins[F("drv")] = bus->getDriverType();
ins[F("text")] = bus->getCustomText();
}
@@ -1016,15 +1024,15 @@ void serializeConfig(JsonObject root) {
JsonArray hw_btn_ins = hw_btn.createNestedArray("ins");
// configuration for all buttons
for (int i = 0; i < WLED_MAX_BUTTONS; i++) {
for (const auto &button : buttons) {
JsonObject hw_btn_ins_0 = hw_btn_ins.createNestedObject();
hw_btn_ins_0["type"] = buttonType[i];
hw_btn_ins_0["type"] = button.type;
JsonArray hw_btn_ins_0_pin = hw_btn_ins_0.createNestedArray("pin");
hw_btn_ins_0_pin.add(btnPin[i]);
hw_btn_ins_0_pin.add(button.pin);
JsonArray hw_btn_ins_0_macros = hw_btn_ins_0.createNestedArray("macros");
hw_btn_ins_0_macros.add(macroButton[i]);
hw_btn_ins_0_macros.add(macroLongPress[i]);
hw_btn_ins_0_macros.add(macroDoublePress[i]);
hw_btn_ins_0_macros.add(button.macroButton);
hw_btn_ins_0_macros.add(button.macroLongPress);
hw_btn_ins_0_macros.add(button.macroDoublePress);
}
hw_btn[F("tt")] = touchThreshold;
@@ -1212,23 +1220,21 @@ void serializeConfig(JsonObject root) {
cntdwn["macro"] = macroCountdown;
JsonArray timers_ins = timers.createNestedArray("ins");
for (unsigned i = 0; i < 10; i++) {
if (timerMacro[i] == 0 && timerHours[i] == 0 && timerMinutes[i] == 0) continue; // sunrise/sunset get saved always (timerHours=255)
JsonObject timers_ins0 = timers_ins.createNestedObject();
timers_ins0["en"] = (timerWeekday[i] & 0x01);
timers_ins0[F("hour")] = timerHours[i];
timers_ins0["min"] = timerMinutes[i];
timers_ins0["macro"] = timerMacro[i];
timers_ins0[F("dow")] = timerWeekday[i] >> 1;
if (i<8) {
JsonObject start = timers_ins0.createNestedObject("start");
start["mon"] = (timerMonth[i] >> 4) & 0xF;
start["day"] = timerDay[i];
JsonObject end = timers_ins0.createNestedObject("end");
end["mon"] = timerMonth[i] & 0xF;
end["day"] = timerDayEnd[i];
}
for (size_t i = 0; i < ::timers.size(); i++) {
const Timer& t = ::timers[i];
if (t.preset == 0 && t.hour == 0 && t.minute == 0) continue;
JsonObject ti = timers_ins.createNestedObject();
ti[F("en")] = t.isEnabled() ? 1 : 0;
ti[F("hour")] = t.hour;
ti[F("min")] = t.minute;
ti[F("macro")] = t.preset;
ti[F("dow")] = t.weekdays >> 1;
JsonObject start = ti.createNestedObject(F("start"));
start[F("mon")] = t.monthStart;
start[F("day")] = t.dayStart;
JsonObject end = ti.createNestedObject(F("end"));
end[F("mon")] = t.monthEnd;
end[F("day")] = t.dayEnd;
}
JsonObject ota = root.createNestedObject("ota");
@@ -1266,7 +1272,7 @@ static const char s_wsec_json[] PROGMEM = "/wsec.json";
bool deserializeConfigSec() {
DEBUG_PRINTLN(F("Reading settings from /wsec.json..."));
if (!requestJSONBufferLock(3)) return false;
if (!requestJSONBufferLock(JSON_LOCK_CFG_SEC_DES)) return false;
bool success = readObjectFromFile(s_wsec_json, nullptr, pDoc);
if (!success) {
@@ -1320,7 +1326,7 @@ bool deserializeConfigSec() {
void serializeConfigSec() {
DEBUG_PRINTLN(F("Writing settings to /wsec.json..."));
if (!requestJSONBufferLock(4)) return;
if (!requestJSONBufferLock(JSON_LOCK_CFG_SEC_SER)) return;
JsonObject root = pDoc->to<JsonObject>();
+149 -82
View File
@@ -1,12 +1,21 @@
#include "wled.h"
#include "fcn_declare.h"
#include "colors.h"
/*
* Color conversion & utility methods
*/
/*
* color blend function, based on FastLED blend function
* the calculation for each color is: result = (A*(amountOfA) + A + B*(amountOfB) + B) / 256 with amountOfA = 255 - amountOfB
* FastLED Reference
* -----------------
* functions in this file derived from FastLED @ 3.6.0 (https://github.com/FastLED/FastLED) are marked with a comment containing "derived from FastLED"
* those functions are therefore licensed under the MIT license See /src/dependencies/fastled_slim/LICENSE.txt for details.
*/
/*
* color blend function
* the calculation for each color is: result = (C1*(256-blend)+C2+C2*blend) / 256
*/
uint32_t WLED_O2_ATTR IRAM_ATTR color_blend(uint32_t color1, uint32_t color2, uint8_t blend) {
// min / max blend checking is omitted: calls with 0 or 255 are rare, checking lowers overall performance
@@ -25,7 +34,7 @@ uint32_t WLED_O2_ATTR IRAM_ATTR color_blend(uint32_t color1, uint32_t color2, ui
* original idea: https://github.com/wled-dev/WLED/pull/2465 by https://github.com/Proto-molecule
* speed optimisations by @dedehai
*/
uint32_t WLED_O2_ATTR color_add(uint32_t c1, uint32_t c2, bool preserveCR) //1212558 | 1212598 | 1212576 | 1212530
uint32_t WLED_O2_ATTR color_add(uint32_t c1, uint32_t c2, bool preserveCR)
{
if (c1 == BLACK) return c2;
if (c2 == BLACK) return c1;
@@ -40,10 +49,9 @@ uint32_t WLED_O2_ATTR color_add(uint32_t c1, uint32_t c2, bool preserveCR) //121
uint32_t b = rb & 0xFFFF;
uint32_t w = wg >> 16;
uint32_t g = wg & 0xFFFF;
uint32_t max = std::max(r,g);
max = std::max(max,b);
max = std::max(max,w);
const uint32_t scale = (uint32_t(255)<<8) / max; // division of two 8bit (shifted) values does not work -> use bit shifts and multiplaction instead
uint32_t maxval = (r > g) ? ((r > b) ? r : b) : ((g > b) ? g : b); // find max value. note: faster than using max() function or on par
maxval = (w > maxval) ? w : maxval; // check white channel as well to avoid division by zero in pure white input
const uint32_t scale = (uint32_t(255)<<8) / maxval; // division of two 8bit (shifted) values does not work -> use bit shifts and multiplaction instead
rb = ((rb * scale) >> 8) & TWO_CHANNEL_MASK;
wg = (wg * scale) & ~TWO_CHANNEL_MASK;
} else wg <<= 8; //shift white and green back to correct position
@@ -60,48 +68,54 @@ uint32_t WLED_O2_ATTR color_add(uint32_t c1, uint32_t c2, bool preserveCR) //121
/*
* fades color toward black
* if using "video" method the resulting color will never become black unless it is already black
* if using "video" method the resulting color will not become black unless it is already black or distorts the hue
*/
uint32_t IRAM_ATTR color_fade(uint32_t c1, uint8_t amount, bool video) {
if (c1 == 0 || amount == 0) return 0; // black or no change
if (amount == 255) return c1;
uint32_t addRemains = 0;
if (!video) amount++; // add one for correct scaling using bitshifts
else {
// video scaling: make sure colors do not dim to zero if they started non-zero unless they distort the hue
uint8_t r = byte(c1>>16), g = byte(c1>>8), b = byte(c1), w = byte(c1>>24); // extract r, g, b, w channels
uint8_t maxc = (r > g) ? ((r > b) ? r : b) : ((g > b) ? g : b); // determine dominant channel for hue preservation
addRemains = r && (r<<5) > maxc ? 0x00010000 : 0; // note: setting color preservation threshold too high results in flickering and
addRemains |= g && (g<<5) > maxc ? 0x00000100 : 0; // jumping colors in low brightness gradients. Multiplying the color preserves
addRemains |= b && (b<<5) > maxc ? 0x00000001 : 0; // better accuracy than dividing the maxc. Shifting by 5 is a good compromise
addRemains |= w ? 0x01000000 : 0; // i.e. remove color channel if <13% of max
}
if (c1 == BLACK || amount == 0) return 0; // black or full fade
if (amount == 255) return c1; // no change
const uint32_t TWO_CHANNEL_MASK = 0x00FF00FF;
uint32_t rb = (((c1 & TWO_CHANNEL_MASK) * amount) >> 8) & TWO_CHANNEL_MASK; // scale red and blue
uint32_t wg = (((c1 >> 8) & TWO_CHANNEL_MASK) * amount) & ~TWO_CHANNEL_MASK; // scale white and green
return (rb | wg) + addRemains;
uint32_t rb = c1 & TWO_CHANNEL_MASK; // extract R and B channels
uint32_t wg = (c1 >> 8) & TWO_CHANNEL_MASK; // extract W and G channels (shifted for multiplication)
uint32_t rb_scaled;
uint32_t wg_scaled;
// video scaling: make sure colors do not dim to zero if they started non-zero unless they distort the hue
if (video) {
rb_scaled = ((rb * amount + 0x007F007F) >> 8) & TWO_CHANNEL_MASK; // scale red and blue, add 0.5 for rounding
wg_scaled = (wg * amount + 0x007F007F) & ~TWO_CHANNEL_MASK; // scale white and green, add 0.5 for rounding
uint8_t r = byte(rb>>16), g = byte(wg), b = byte(rb), w = byte(wg>>16); // extract r, g, b, w channels from original color (wg is shifted)
uint8_t maxc = (r > g) ? ((r > b) ? r : b) : ((g > b) ? g : b); // determine dominant channel for hue preservation
maxc = (maxc>>2) + 1; // divide by 4 to get ~25% threshold for hue preservation, add 1 to prevent "washout" of very dark colors (prevents them becoming gray)
rb_scaled |= r > maxc ? 0x00010000 : 0;
wg_scaled |= g > maxc ? 0x00000100 : 0;
rb_scaled |= b > maxc ? 0x00000001 : 0;
wg_scaled |= w ? 0x01000000 : 0; // preserve white if it is present
} else {
rb_scaled = ((rb * (amount + 1)) >> 8) & TWO_CHANNEL_MASK; // scale red and blue
wg_scaled = ((wg * (amount + 1)) & ~TWO_CHANNEL_MASK); // scale white and green
}
return (rb_scaled | wg_scaled);
}
/*
* color adjustment in HSV color space (converts RGB to HSV and back), color conversions are not 100% accurate!
shifts hue, increase brightness, decreases saturation (if not black)
note: inputs are 32bit to speed up the function, useful input value ranges are 0-255
* shifts hue, increase brightness, decreases saturation (if not black)
* note: inputs are 32bit to speed up the function, useful input value ranges are -255 to +255
* note2: if only one hue change is needed, use CRGBW.adjust_hue() instead (much faster)
*/
uint32_t adjust_color(uint32_t rgb, uint32_t hueShift, uint32_t lighten, uint32_t brighten) {
if (rgb == 0 || hueShift + lighten + brighten == 0) return rgb; // black or no change
WLED_O3_ATTR void adjust_color(CRGBW& rgb, int32_t hueShift, int32_t satChange, int32_t valueChange) {
if(rgb.color32 == 0 && valueChange <= 0) return; // black and no value change -> return black
CHSV32 hsv;
rgb2hsv(rgb, hsv); //convert to HSV
hsv.h += (hueShift << 8); // shift hue (hue is 16 bits)
hsv.s = max((int32_t)0, (int32_t)hsv.s - (int32_t)lighten); // desaturate
hsv.v = min((uint32_t)255, (uint32_t)hsv.v + brighten); // increase brightness
uint32_t rgb_adjusted;
hsv2rgb(hsv, rgb_adjusted); // convert back to RGB TODO: make this into 16 bit conversion
return rgb_adjusted;
hsv.s = (int)hsv.s + satChange < 0 ? 0 : ((int)hsv.s + satChange > 255 ? 255 : (int)hsv.s + satChange);
hsv.v = (int)hsv.v + valueChange < 0 ? 0 : ((int)hsv.v + valueChange > 255 ? 255 : (int)hsv.v + valueChange);
hsv2rgb_spectrum(hsv, rgb); // convert back to RGB
}
// 1:1 replacement of fastled function optimized for ESP, slightly faster, more accurate and uses less flash (~ -200bytes)
uint32_t ColorFromPaletteWLED(const CRGBPalette16& pal, unsigned index, uint8_t brightness, TBlendType blendType) {
// derived from FastLED: replacement of fastled function optimized for ESP, slightly faster, more accurate and uses less flash (~ -200bytes)
uint32_t ColorFromPalette(const CRGBPalette16& pal, unsigned index, uint8_t brightness, TBlendType blendType) {
if (blendType == LINEARBLEND_NOWRAP) {
index = (index * 0xF0) >> 8; // Blend range is affected by lo4 blend of values, remap to avoid wrapping
}
@@ -249,12 +263,16 @@ void loadCustomPalettes() {
byte tcp[72]; //support gradient palettes with up to 18 entries
CRGBPalette16 targetPalette;
customPalettes.clear(); // start fresh
StaticJsonDocument<1536> pDoc; // barely enough to fit 72 numbers -> TODO: current format uses 214 bytes max per palette, why is this buffer so large?
unsigned emptyPaletteGap = 0; // count gaps in palette files to stop looking for more (each exists() call takes ~5ms)
for (int index = 0; index < WLED_MAX_CUSTOM_PALETTES; index++) {
char fileName[32];
sprintf_P(fileName, PSTR("/palette%d.json"), index);
StaticJsonDocument<1536> pDoc; // barely enough to fit 72 numbers
if (WLED_FS.exists(fileName)) {
// add gray placeholders to preserve palette IDs for subsequent slots (is omitted in UI but shown in cpal.htm)
for (unsigned g = 0; g < emptyPaletteGap; g++)
customPalettes.push_back(CRGBPalette16(CRGB(128, 128, 128)));
emptyPaletteGap = 0; // reset gap counter if file exists
DEBUGFX_PRINTF_P(PSTR("Reading palette from %s\n"), fileName);
if (readObjectFromFile(fileName, nullptr, &pDoc)) {
JsonArray pal = pDoc[F("palette")];
@@ -288,69 +306,118 @@ void loadCustomPalettes() {
}
}
} else {
break;
emptyPaletteGap++;
if (emptyPaletteGap > WLED_MAX_CUSTOM_PALETTE_GAP) break; // stop looking for more palettes
}
}
}
void hsv2rgb(const CHSV32& hsv, uint32_t& rgb) // convert HSV (16bit hue) to RGB (32bit with white = 0)
{
unsigned int remainder, region, p, q, t;
unsigned int h = hsv.h;
unsigned int s = hsv.s;
unsigned int v = hsv.v;
if (s == 0) {
rgb = v << 16 | v << 8 | v;
return;
size_t removeUsermodPalettes(const char *name) {
size_t before = usermodPalettes.size();
for (int i = usermodPalettes.size() - 1; i >= 0; i--) {
if (usermodPalettes[i].name == name)
usermodPalettes.erase(usermodPalettes.begin() + i);
}
region = h / 10923; // 65536 / 6 = 10923
remainder = (h - (region * 10923)) * 6;
p = (v * (255 - s)) >> 8;
q = (v * (255 - ((s * remainder) >> 16))) >> 8;
t = (v * (255 - ((s * (65535 - remainder)) >> 16))) >> 8;
return before - usermodPalettes.size();
}
// convert HSV (16bit hue) to RGB (32bit with white = 0), optimized for speed
WLED_O2_ATTR void hsv2rgb_spectrum(const CHSV32& hsv, CRGBW& rgb) {
unsigned p, q, t;
unsigned region = ((unsigned)hsv.h * 6) >> 16; // h / (65536 / 6)
unsigned remainder = (hsv.h - (region * 10923)) * 6; // 10923 = (65536 / 6)
// check for zero saturation
if (hsv.s == 0) {
rgb.r = rgb.g = rgb.b = hsv.v;
return;
}
p = (hsv.v * (255 - hsv.s)) >> 8;
q = (hsv.v * (255 - ((hsv.s * remainder) >> 16))) >> 8;
t = (hsv.v * (255 - ((hsv.s * (65535 - remainder)) >> 16))) >> 8;
switch (region) {
case 0:
rgb = v << 16 | t << 8 | p; break;
rgb.r = hsv.v;
rgb.g = t;
rgb.b = p;
break;
case 1:
rgb = q << 16 | v << 8 | p; break;
rgb.r = q;
rgb.g = hsv.v;
rgb.b = p;
break;
case 2:
rgb = p << 16 | v << 8 | t; break;
rgb.r = p;
rgb.g = hsv.v;
rgb.b = t;
break;
case 3:
rgb = p << 16 | q << 8 | v; break;
rgb.r = p;
rgb.g = q;
rgb.b = hsv.v;
break;
case 4:
rgb = t << 16 | p << 8 | v; break;
rgb.r = t;
rgb.g = p;
rgb.b = hsv.v;
break;
default:
rgb = v << 16 | p << 8 | q; break;
rgb.r = hsv.v;
rgb.g = p;
rgb.b = q;
break;
}
}
void rgb2hsv(const uint32_t rgb, CHSV32& hsv) // convert RGB to HSV (16bit hue), much more accurate and faster than fastled version
{
hsv.raw = 0;
int32_t r = (rgb>>16)&0xFF;
int32_t g = (rgb>>8)&0xFF;
int32_t b = rgb&0xFF;
int32_t minval, maxval, delta;
minval = min(r, g);
minval = min(minval, b);
maxval = max(r, g);
maxval = max(maxval, b);
if (maxval == 0) return; // black
hsv.v = maxval;
delta = maxval - minval;
// CHSV to CRGB wrapper conversion: slower so this should not be used in time critical code, use rainbow version instead
void hsv2rgb_spectrum(const CHSV& hsv, CRGB& rgb) {
CHSV32 hsv32(hsv);
CRGBW rgb32;
hsv2rgb_spectrum(hsv32, rgb32);
rgb = CRGB(rgb32);
}
// convert RGB to HSV (16bit hue), not 100% color accurate. note: using "O3" makes it ~5% faster at minimal flash cost (~20 bytes)
WLED_O3_ATTR void rgb2hsv(const CRGBW& rgb, CHSV32& hsv) {
int32_t r = rgb.r; // note: using 32bit variables tested faster than 8bit
int32_t g = rgb.g;
int32_t b = rgb.b;
uint32_t minval, maxval;
int32_t delta;
// find min/max value. note: faster than using min/max functions (lets compiler optimize more when using "O3"), other variants (nested ifs, xor) tested slower
maxval = (r > g) ? ((r > b) ? r : b) : ((g > b) ? g : b);
if (maxval == 0) {
hsv.hsv32 = 0;
return; // black, avoids division by zero
}
minval = (r < g) ? ((r < b) ? r : b) : ((g < b) ? g : b);
hsv.v = maxval;
delta = maxval - minval;
if (delta != 0) {
hsv.s = (255 * delta) / maxval;
if (hsv.s == 0) return; // gray value
if (maxval == r) hsv.h = (10923 * (g - b)) / delta;
else if (maxval == g) hsv.h = 21845 + (10923 * (b - r)) / delta;
else hsv.h = 43690 + (10923 * (r - g)) / delta;
// note: early return if s==0 is omitted here to increase speed as gray values are rarely used
if (maxval == r) hsv.h = (uint16_t)((10923 * (g - b)) / delta);
else if (maxval == g) hsv.h = (uint16_t)(21845 + (10923 * (b - r)) / delta);
else hsv.h = (uint16_t)(43690 + (10923 * (r - g)) / delta);
} else {
hsv.s = 0;
hsv.h = 0; // gray, hue is undefined but set to 0 for consistency
}
}
CHSV rgb2hsv(const CRGB c) { // CRGB to CHSV
CHSV32 hsv;
rgb2hsv(CRGBW(c), hsv);
return CHSV(hsv);
}
void colorHStoRGB(uint16_t hue, byte sat, byte* rgb) { //hue, sat to rgb
uint32_t crgb;
hsv2rgb(CHSV32(hue, sat, 255), crgb);
rgb[0] = byte((crgb) >> 16);
rgb[1] = byte((crgb) >> 8);
rgb[2] = byte(crgb);
CRGBW crgb;
hsv2rgb_spectrum(CHSV32(hue, sat, 255), crgb);
rgb[0] = crgb.r;
rgb[1] = crgb.g;
rgb[2] = crgb.b;
}
//get RGB values from color temperature in K (https://tannerhelland.com/2012/09/18/convert-temperature-rgb-algorithm-code.html)
+158 -91
View File
@@ -1,96 +1,34 @@
#pragma once
#ifndef WLED_COLORS_H
#define WLED_COLORS_H
#include <vector>
#include "src/dependencies/fastled_slim/fastled_slim.h"
/*
* Color structs and color utility functions
*/
#include <vector>
#include "FastLED.h"
/*
Note on color types and conversions:
- WLED uses 32bit colors (RGBW), if possible, use CRGBW instead of CRGB for better performance (no conversion in setPixelColor)
- use CRGB if RAM usage is of concern (i.e. for larger color arrays)
- direct conversion (assignment or construction) from CHSV/CHSV32 to CRGB/CRGBW use the "rainbow" method (nicer colors, see fastled documentation)
- converting CRGB(W) to HSV32 color is quite accurate but still not 100% (but much more accurate than fastled's "hsv2rgb_approximate" function)
- when converting CRGB(W) to HSV32 and back, "hsv2rgb_spectrum" preserves the colors better than the _rainbow version
- to manipulate an RGB color in HSV space, use the adjust_color function or the CRGBW.adjust_hue method
#define ColorFromPalette ColorFromPaletteWLED // override fastled version
Some functions in this file are derived from FastLED (https://github.com/FastLED/FastLED) licensed under the MIT license.
See /src/dependencies/fastled_slim/LICENSE.txt for details.
*/
// CRGBW can be used to manipulate 32bit colors faster. However: if it is passed to functions, it adds overhead compared to a uint32_t color
// use with caution and pay attention to flash size. Usually converting a uint32_t to CRGBW to extract r, g, b, w values is slower than using bitshifts
// it can be useful to avoid back and forth conversions between uint32_t and fastled CRGB
struct CRGBW {
union {
uint32_t color32; // Access as a 32-bit value (0xWWRRGGBB)
struct {
uint8_t b;
uint8_t g;
uint8_t r;
uint8_t w;
};
uint8_t raw[4]; // Access as an array in the order B, G, R, W
};
// 32bit color mangling macros
#define RGBW32(r,g,b,w) (uint32_t((byte(w) << 24) | (byte(r) << 16) | (byte(g) << 8) | (byte(b))))
#define R(c) (byte((c) >> 16))
#define G(c) (byte((c) >> 8))
#define B(c) (byte(c))
#define W(c) (byte((c) >> 24))
// Default constructor
inline CRGBW() __attribute__((always_inline)) = default;
struct CRGBW; // forward declations
struct CHSV32;
// Constructor from a 32-bit color (0xWWRRGGBB)
constexpr CRGBW(uint32_t color) __attribute__((always_inline)) : color32(color) {}
// Constructor with r, g, b, w values
constexpr CRGBW(uint8_t red, uint8_t green, uint8_t blue, uint8_t white = 0) __attribute__((always_inline)) : b(blue), g(green), r(red), w(white) {}
// Constructor from CRGB
constexpr CRGBW(CRGB rgb) __attribute__((always_inline)) : b(rgb.b), g(rgb.g), r(rgb.r), w(0) {}
// Access as an array
inline const uint8_t& operator[] (uint8_t x) const __attribute__((always_inline)) { return raw[x]; }
// Assignment from 32-bit color
inline CRGBW& operator=(uint32_t color) __attribute__((always_inline)) { color32 = color; return *this; }
// Assignment from r, g, b, w
inline CRGBW& operator=(const CRGB& rgb) __attribute__((always_inline)) { b = rgb.b; g = rgb.g; r = rgb.r; w = 0; return *this; }
// Conversion operator to uint32_t
inline operator uint32_t() const __attribute__((always_inline)) {
return color32;
}
/*
// Conversion operator to CRGB
inline operator CRGB() const __attribute__((always_inline)) {
return CRGB(r, g, b);
}
CRGBW& scale32 (uint8_t scaledown) // 32bit math
{
if (color32 == 0) return *this; // 2 extra instructions, worth it if called a lot on black (which probably is true) adding check if scaledown is zero adds much more overhead as its 8bit
uint32_t scale = scaledown + 1;
uint32_t rb = (((color32 & 0x00FF00FF) * scale) >> 8) & 0x00FF00FF; // scale red and blue
uint32_t wg = (((color32 & 0xFF00FF00) >> 8) * scale) & 0xFF00FF00; // scale white and green
color32 = rb | wg;
return *this;
}*/
};
struct CHSV32 { // 32bit HSV color with 16bit hue for more accurate conversions
union {
struct {
uint16_t h; // hue
uint8_t s; // saturation
uint8_t v; // value
};
uint32_t raw; // 32bit access
};
inline CHSV32() __attribute__((always_inline)) = default; // default constructor
/// Allow construction from hue, saturation, and value
/// @param ih input hue
/// @param is input saturation
/// @param iv input value
inline CHSV32(uint16_t ih, uint8_t is, uint8_t iv) __attribute__((always_inline)) // constructor from 16bit h, s, v
: h(ih), s(is), v(iv) {}
inline CHSV32(uint8_t ih, uint8_t is, uint8_t iv) __attribute__((always_inline)) // constructor from 8bit h, s, v
: h((uint16_t)ih << 8), s(is), v(iv) {}
inline CHSV32(const CHSV& chsv) __attribute__((always_inline)) // constructor from CHSV
: h((uint16_t)chsv.h << 8), s(chsv.s), v(chsv.v) {}
inline operator CHSV() const { return CHSV((uint8_t)(h >> 8), s, v); } // typecast to CHSV
};
extern bool gammaCorrectCol;
// similar to NeoPixelBus NeoGammaTableMethod but allows dynamic changes (superseded by NPB::NeoGammaDynamicTableMethod)
class NeoGammaWLEDMethod {
@@ -118,18 +56,31 @@ class NeoGammaWLEDMethod {
inline uint32_t color_blend16(uint32_t c1, uint32_t c2, uint16_t b) { return color_blend(c1, c2, b >> 8); };
[[gnu::hot, gnu::pure]] uint32_t color_add(uint32_t, uint32_t, bool preserveCR = false);
[[gnu::hot, gnu::pure]] uint32_t color_fade(uint32_t c1, uint8_t amount, bool video = false);
[[gnu::hot, gnu::pure]] uint32_t adjust_color(uint32_t rgb, uint32_t hueShift, uint32_t lighten, uint32_t brighten);
[[gnu::hot, gnu::pure]] uint32_t ColorFromPaletteWLED(const CRGBPalette16 &pal, unsigned index, uint8_t brightness = (uint8_t)255U, TBlendType blendType = LINEARBLEND);
void adjust_color(CRGBW& rgb, int32_t hueShift, int32_t satChange,int32_t valueChange);
[[gnu::hot, gnu::pure]] uint32_t ColorFromPalette(const CRGBPalette16 &pal, unsigned index, uint8_t brightness = (uint8_t)255U, TBlendType blendType = LINEARBLEND);
CRGBPalette16 generateHarmonicRandomPalette(const CRGBPalette16 &basepalette);
CRGBPalette16 generateRandomPalette();
// Palette registered by a usermod at fixed IDs (255, 254, 253... 201).
// Display name is "name: palName" (if palName non-null) or falls back to "name index" (e.g. "AudioReactive 1"), see util.cpp
struct UsermodPalette {
CRGBPalette16 palette;
const char *name; // PROGMEM base name string (must not be nullptr), this name is used in removeUsermodPalettes()
uint8_t palIndex; // index of the palette for a usermod
const char *palName; // optional PROGMEM display name; if set, shown as "name: palName" (e.g. "AudioReactive: Audio Responsive Hue"), otherwise falls back to "name index"
};
void loadCustomPalettes();
size_t removeUsermodPalettes(const char *name); // remove all entries from usermodPalettes whose name pointer matches 'name'
extern std::vector<CRGBPalette16> customPalettes;
inline size_t getPaletteCount() { return FIXED_PALETTE_COUNT + customPalettes.size(); }
inline uint32_t colorFromRgbw(byte* rgbw) { return uint32_t((byte(rgbw[3]) << 24) | (byte(rgbw[0]) << 16) | (byte(rgbw[1]) << 8) | (byte(rgbw[2]))); }
void hsv2rgb(const CHSV32& hsv, uint32_t& rgb);
extern std::vector<UsermodPalette> usermodPalettes;
inline size_t getPaletteCount() { return FIXED_PALETTE_COUNT + usermodPalettes.size() + customPalettes.size(); }
void hsv2rgb_spectrum(const CHSV32& hsv, CRGBW& rgb);
void hsv2rgb_spectrum(const CHSV& hsv, CRGB& rgb);
void rgb2hsv(const CRGBW& rgb, CHSV32& hsv);
CHSV rgb2hsv(const CRGB c);
void colorHStoRGB(uint16_t hue, byte sat, byte* rgb);
void rgb2hsv(const uint32_t rgb, CHSV32& hsv);
inline CHSV rgb2hsv(const CRGB c) { CHSV32 hsv; rgb2hsv((uint32_t((byte(c.r) << 16) | (byte(c.g) << 8) | (byte(c.b)))), hsv); return CHSV(hsv); } // CRGB to hsv
void colorKtoRGB(uint16_t kelvin, byte* rgb);
void colorCTtoRGB(uint16_t mired, byte* rgb); //white spectrum to rgb
void colorXYtoRGB(float x, float y, byte* rgb); // only defined if huesync disabled TODO
@@ -149,7 +100,123 @@ static inline uint32_t fast_color_scale(const uint32_t c, const uint8_t scale) {
}
// palettes
extern const TProgmemRGBPalette16 PartyColors_gc22 PROGMEM;
extern const TProgmemRGBPalette16* const fastledPalettes[];
extern const uint8_t* const gGradientPalettes[];
#endif
struct CHSV32 { // 32bit HSV color with 16bit hue for more accurate conversions
union {
struct {
uint16_t h; // hue
uint8_t s; // saturation
uint8_t v; // value
};
uint32_t hsv32; // 32bit access
};
inline CHSV32() __attribute__((always_inline)) = default; // default constructor
// allow construction from hue (ih), saturation (is), and value (iv)
inline CHSV32(uint16_t ih, uint8_t is, uint8_t iv) __attribute__((always_inline)) // constructor from 16bit h, s, v
: h(ih), s(is), v(iv) {}
inline CHSV32(uint8_t ih, uint8_t is, uint8_t iv) __attribute__((always_inline)) // constructor from 8bit h, s, v
: h((uint16_t)ih << 8), s(is), v(iv) {}
inline CHSV32(const CHSV& chsv) __attribute__((always_inline)) // constructor from CHSV
: h((uint16_t)chsv.h << 8), s(chsv.s), v(chsv.v) {}
inline operator CHSV() const { return CHSV((uint8_t)(h >> 8), s, v); } // typecast to CHSV
// construction from a 32bit rgb color (white channel is ignored)
inline CHSV32(const CRGBW& rgb) __attribute__((always_inline));
inline CHSV32& operator= (const CRGBW& rgb) __attribute__((always_inline)); // assignment from 32bit rgb color (white channel is ignored)
};
// CRGBW can be used to manipulate 32bit colors faster. However: if it is passed to functions, it adds overhead compared to a uint32_t color
// use with caution and pay attention to flash size. Usually converting a uint32_t to CRGBW to extract r, g, b, w values is slower than using bitshifts
// it can be useful to avoid back and forth conversions between uint32_t and fastled CRGB
struct CRGBW {
union {
uint32_t color32; // Access as a 32-bit value (0xWWRRGGBB)
struct {
uint8_t b;
uint8_t g;
uint8_t r;
uint8_t w;
};
uint8_t raw[4]; // Access as an array in the order B, G, R, W (matches 32 bit colors)
};
// Default constructor
inline CRGBW() __attribute__((always_inline)) = default;
// Constructor from a 32-bit color (0xWWRRGGBB)
constexpr CRGBW(uint32_t color) __attribute__((always_inline)) : color32(color) {}
// Constructor with r, g, b, w values
constexpr CRGBW(uint8_t red, uint8_t green, uint8_t blue, uint8_t white = 0) __attribute__((always_inline)) : b(blue), g(green), r(red), w(white) {}
// Constructor from CRGB
constexpr CRGBW(CRGB rgb) __attribute__((always_inline)) : b(rgb.b), g(rgb.g), r(rgb.r), w(0) {}
// Constructor from CHSV32
inline CRGBW(CHSV32 hsv) __attribute__((always_inline)) { hsv2rgb_rainbow(hsv.h, hsv.s, hsv.v, raw, true); }
// Constructor from CHSV
inline CRGBW(CHSV hsv) __attribute__((always_inline)) { hsv2rgb_rainbow(hsv.h<<8, hsv.s, hsv.v, raw, true); }
// Access as an array
inline const uint8_t& operator[](uint8_t x) const __attribute__((always_inline)) { return raw[x]; }
// Assignment from 32-bit color
inline CRGBW& operator=(uint32_t color) __attribute__((always_inline)) { color32 = color; return *this; }
// Assignment from CHSV32
inline CRGBW& operator=(CHSV32 hsv) __attribute__((always_inline)) { hsv2rgb_rainbow(hsv.h, hsv.s, hsv.v, raw, true); return *this; }
// Assignment from CHSV
inline CRGBW& operator=(CHSV hsv) __attribute__((always_inline)) { hsv2rgb_rainbow(hsv.h<<8, hsv.s, hsv.v, raw, true); return *this; }
// Assignment from r, g, b, w
inline CRGBW& operator=(const CRGB& rgb) __attribute__((always_inline)) { b = rgb.b; g = rgb.g; r = rgb.r; w = 0; return *this; }
// Conversion operator to uint32_t
inline operator uint32_t() const __attribute__((always_inline)) {
return color32;
}
// adjust hue: input range is 256 for full color cycle, input can be negative
inline void adjust_hue(int hueshift) __attribute__((always_inline)) {
CHSV32 hsv = *this;
hsv.h += hueshift << 8;
hsv2rgb_spectrum(hsv, *this);
}
// get the average of the R, G, B and W values
uint8_t getAverageLight() const {
return (r + g + b + w) >> 2;
}
// get the average of the R, G, B values
uint8_t getRGBaverage() const {
return ((r + g + b) * 21846) >> 16; // x*21846>>16 is equal to "divide by 3"
}
};
inline CHSV32::CHSV32(const CRGBW& rgb) {
rgb2hsv(rgb, *this);
}
inline CHSV32& CHSV32::operator= (const CRGBW& rgb) { // assignment from 32bit rgb color (white channel is ignored)
rgb2hsv(rgb, *this);
return *this;
}
// explicit hsv2rgb conversions for compatibility
inline CRGBW hsv2rgb(const CHSV32& hsv) { return CRGBW(hsv); }
inline void hsv2rgb(const CHSV32& hsv, CRGBW& rgb) { rgb = CRGBW(hsv); }
inline void hsv2rgb(const CHSV32& hsv, uint32_t& rgb) { rgb = CRGBW(hsv).color32; }
#endif
+106 -33
View File
@@ -6,15 +6,24 @@
* Readability defines and their associated numerical values + compile-time constants
*/
constexpr size_t FASTLED_PALETTE_COUNT = 7; // = sizeof(fastledPalettes) / sizeof(fastledPalettes[0]);
constexpr size_t GRADIENT_PALETTE_COUNT = 59; // = sizeof(gGradientPalettes) / sizeof(gGradientPalettes[0]);
constexpr size_t DYNAMIC_PALETTE_COUNT = 5; // 1-5 are dynamic palettes (1=random,2=primary,3=primary+secondary,4=primary+secondary+tertiary,5=primary+secondary(+tertiary if not black)
constexpr size_t FASTLED_PALETTE_COUNT = 7; // 6-12 = sizeof(fastledPalettes) / sizeof(fastledPalettes[0]);
constexpr size_t GRADIENT_PALETTE_COUNT = 59; // 13-72 = sizeof(gGradientPalettes) / sizeof(gGradientPalettes[0]);
constexpr size_t DYNAMIC_PALETTE_COUNT = 6; // 0- 5 = dynamic palettes (0=default(virtual),1=random,2=primary,3=primary+secondary,4=primary+secondary+tertiary,5=primary+secondary(+tertiary if not black)
constexpr size_t FIXED_PALETTE_COUNT = DYNAMIC_PALETTE_COUNT + FASTLED_PALETTE_COUNT + GRADIENT_PALETTE_COUNT; // total number of fixed palettes
// Palette ID space layout (palette IDs are uint8_t, 0-255):
// 0 .. FIXED_PALETTE_COUNT-1 : fixed built-in palettes
// 72 .. WLED_CUSTOM_PALETTE_ID_BASE(200) : user custom palettes (index 0 = ID 200, growing downward)
// 201.. WLED_USERMOD_PALETTE_ID_BASE(255): usermod-registered palettes (index 0 = ID 255, growing downward)
constexpr uint8_t WLED_USERMOD_PALETTE_ID_BASE = 255; // highest ID for usermod palettes
constexpr uint8_t WLED_CUSTOM_PALETTE_ID_BASE = 200; // highest ID for user custom palettes
constexpr size_t WLED_MAX_USERMOD_PALETTES = WLED_USERMOD_PALETTE_ID_BASE - WLED_CUSTOM_PALETTE_ID_BASE; // 55 slots (IDs 201-255)
#ifndef ESP8266
#define WLED_MAX_CUSTOM_PALETTES (255 - FIXED_PALETTE_COUNT) // allow up to 255 total palettes, user is warned about stability issues when adding more than 10
#define WLED_MAX_CUSTOM_PALETTES (WLED_CUSTOM_PALETTE_ID_BASE - FIXED_PALETTE_COUNT + 1) // 129 slots (IDs 72-200)
#else
#define WLED_MAX_CUSTOM_PALETTES 10 // ESP8266: limit custom palettes to 10
#endif
#define WLED_MAX_CUSTOM_PALETTE_GAP 20 // max number of empty palette files in a row before stopping to look for more (20 takes 100ms)
// You can define custom product info from build flags.
// This is useful to allow API consumer to identify what type of WLED version
@@ -55,32 +64,39 @@ constexpr size_t FIXED_PALETTE_COUNT = DYNAMIC_PALETTE_COUNT + FASTLED_PALETTE_C
#ifdef ESP8266
#define WLED_MAX_DIGITAL_CHANNELS 3
#define WLED_MAX_RMT_CHANNELS 0 // ESP8266 does not have RMT nor I2S
#define WLED_MAX_I2S_CHANNELS 0
#define WLED_MAX_ANALOG_CHANNELS 5
#define WLED_MIN_VIRTUAL_BUSSES 3 // no longer used for bus creation but used to distinguish S2/S3 in UI
#define WLED_MAX_TIMERS 16 // reduced limit for ESP8266 due to memory constraints
#define WLED_PLATFORM_ID 0 // used in UI to distinguish ESP types, needs a proper fix!
#else
#if !defined(LEDC_CHANNEL_MAX) || !defined(LEDC_SPEED_MODE_MAX)
#include "driver/ledc.h" // needed for analog/LEDC channel counts
#endif
#define WLED_MAX_ANALOG_CHANNELS (LEDC_CHANNEL_MAX*LEDC_SPEED_MODE_MAX)
#if defined(CONFIG_IDF_TARGET_ESP32C3) // 2 RMT, 6 LEDC, only has 1 I2S but NPB does not support it ATM
#define WLED_MAX_DIGITAL_CHANNELS 2
#define WLED_MAX_RMT_CHANNELS 2 // ESP32-C3 has 2 RMT output channels
#define WLED_MAX_I2S_CHANNELS 0 // I2S not supported by NPB
//#define WLED_MAX_ANALOG_CHANNELS 6
#define WLED_MIN_VIRTUAL_BUSSES 4 // no longer used for bus creation but used to distinguish S2/S3 in UI
#define WLED_PLATFORM_ID 1 // used in UI to distinguish ESP types, needs a proper fix!
#elif defined(CONFIG_IDF_TARGET_ESP32S2) // 4 RMT, 8 LEDC, only has 1 I2S bus, supported in NPB
// the 5th bus (I2S) will prevent Audioreactive usermod from functioning (it is last used though)
#define WLED_MAX_DIGITAL_CHANNELS 12 // x4 RMT + x1/x8 I2S0
#define WLED_MAX_RMT_CHANNELS 4 // ESP32-S2 has 4 RMT output channels
#define WLED_MAX_I2S_CHANNELS 8 // I2S parallel output supported by NPB
//#define WLED_MAX_ANALOG_CHANNELS 8
#define WLED_MIN_VIRTUAL_BUSSES 4 // no longer used for bus creation but used to distinguish S2/S3 in UI
#define WLED_PLATFORM_ID 2 // used in UI to distinguish ESP type in UI
#elif defined(CONFIG_IDF_TARGET_ESP32S3) // 4 RMT, 8 LEDC, has 2 I2S but NPB supports parallel x8 LCD on I2S1
#define WLED_MAX_DIGITAL_CHANNELS 12 // x4 RMT + x8 I2S-LCD
#define WLED_MAX_RMT_CHANNELS 4 // ESP32-S3 has 4 RMT output channels
#define WLED_MAX_I2S_CHANNELS 8 // uses LCD parallel output not I2S
//#define WLED_MAX_ANALOG_CHANNELS 8
#define WLED_MIN_VIRTUAL_BUSSES 6 // no longer used for bus creation but used to distinguish S2/S3 in UI
#define WLED_PLATFORM_ID 3 // used in UI to distinguish ESP type in UI, needs a proper fix!
#else
// the last digital bus (I2S0) will prevent Audioreactive usermod from functioning
#define WLED_MAX_DIGITAL_CHANNELS 16 // x1/x8 I2S1 + x8 RMT
#define WLED_MAX_RMT_CHANNELS 8 // ESP32 has 8 RMT output channels
#define WLED_MAX_I2S_CHANNELS 8 // I2S parallel output supported by NPB
//#define WLED_MAX_ANALOG_CHANNELS 16
#define WLED_MIN_VIRTUAL_BUSSES 6 // no longer used for bus creation but used to distinguish S2/S3 in UI
#define WLED_PLATFORM_ID 4 // used in UI to distinguish ESP type in UI, needs a proper fix!
#endif
#define WLED_MAX_TIMERS 64 // maximum number of timers
#define WLED_MAX_DIGITAL_CHANNELS (WLED_MAX_RMT_CHANNELS + WLED_MAX_I2S_CHANNELS)
#endif
// WLED_MAX_BUSSES was used to define the size of busses[] array which is no longer needed
// instead it will help determine max number of buses that can be defined at compile time
@@ -102,9 +118,9 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit");
#ifndef WLED_MAX_BUTTONS
#ifdef ESP8266
#define WLED_MAX_BUTTONS 2
#define WLED_MAX_BUTTONS 10
#else
#define WLED_MAX_BUTTONS 4
#define WLED_MAX_BUTTONS 32
#endif
#else
#if WLED_MAX_BUTTONS < 2
@@ -113,6 +129,8 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit");
#endif
#endif
#define RELAY_DELAY 50 // delay in ms between switching on relay and sending data to LEDs
#if defined(ESP8266) || defined(CONFIG_IDF_TARGET_ESP32S2)
#define WLED_MAX_COLOR_ORDER_MAPPINGS 5
#else
@@ -147,9 +165,14 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit");
#define WLED_MAX_PANELS 18 // must not be more than 32
//Usermod IDs
#define USERMOD_ID_RESERVED 0 //Unused. Might indicate no usermod present
#define USERMOD_ID_UNSPECIFIED 1 //Default value for a general user mod that does not specify a custom ID
// Usermod IDs
// A unique ID is only required when a usermod needs one or more of:
// 1. Inter-usermod communication: UsermodManager::lookup(mod_id) or getUMData(..., mod_id)
// 2. Pin ownership via pinManager: PinOwner enum entries map to these IDs (see pin_manager.h)
// 3. Identification in JSON info: addToJsonInfo emits each mod's ID into the "um" array
// If none of the above apply, omit getId() (or return USERMOD_ID_UNSPECIFIED) and do NOT add an entry here.
#define USERMOD_ID_RESERVED 0 //Unused. Reserved; may indicate no usermod present
#define USERMOD_ID_UNSPECIFIED 1 //Default for usermods that do not require a unique ID
#define USERMOD_ID_EXAMPLE 2 //Usermod "usermod_v2_example.h"
#define USERMOD_ID_TEMPERATURE 3 //Usermod "usermod_temperature.h"
#define USERMOD_ID_FIXNETSERVICES 4 //Usermod "usermod_Fix_unreachable_netservices.h"
@@ -208,6 +231,12 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit");
#define USERMOD_ID_BRIGHTNESS_FOLLOW_SUN 57 //Usermod "usermod_v2_brightness_follow_sun.h"
#define USERMOD_ID_USER_FX 58 //Usermod "user_fx"
//Wifi encryption type
#ifdef WLED_ENABLE_WPA_ENTERPRISE
#define WIFI_ENCRYPTION_TYPE_PSK 0 //None/WPA/WPA2
#define WIFI_ENCRYPTION_TYPE_ENTERPRISE 1 //WPA/WPA2-Enterprise
#endif
//Access point behavior
#define AP_BEHAVIOR_BOOT_NO_CONN 0 //Open AP when no connection after boot
#define AP_BEHAVIOR_NO_CONN 1 //Open when no connection (either after boot or if connection is lost)
@@ -290,7 +319,7 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit");
#define TYPE_DIGITAL_MIN 16 // first usable digital type
#define TYPE_WS2812_1CH 18 //white-only chips (1 channel per IC) (unused)
#define TYPE_WS2812_1CH_X3 19 //white-only chips (3 channels per IC)
#define TYPE_WS2812_2CH_X3 20 //CCT chips (1st IC controls WW + CW of 1st zone and CW of 2nd zone, 2nd IC controls WW of 2nd zone and WW + CW of 3rd zone)
//#define TYPE_WS2812_2CH_X3 20 // use FW1906
#define TYPE_WS2812_WWA 21 //amber + warm + cold white
#define TYPE_WS2812_RGB 22
#define TYPE_GS8608 23 //same driver as WS2812, but will require signal 2x per second (else displays test pattern)
@@ -299,7 +328,7 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit");
#define TYPE_UCS8903 26
#define TYPE_APA106 27
#define TYPE_FW1906 28 //RGB + CW + WW + unused channel (6 channels per IC)
#define TYPE_UCS8904 29 //first RGBW digital type (hardcoded in busmanager.cpp, memUsage())
#define TYPE_UCS8904 29 //first RGBW digital type (hardcoded in busmanager.cpp)
#define TYPE_SK6812_RGBW 30
#define TYPE_TM1814 31
#define TYPE_WS2805 32 //RGB + WW + CW
@@ -366,7 +395,8 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit");
#define BTN_TYPE_TOUCH_SWITCH 9
//Ethernet board types
#define WLED_NUM_ETH_TYPES 13
#define WLED_NUM_ETH_TYPES 16
#define WLED_ETH_NONE 0
#define WLED_ETH_WT32_ETH01 1
@@ -381,6 +411,10 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit");
#define WLED_ETH_SERG74 10
#define WLED_ETH_ESP32_POE_WROVER 11
#define WLED_ETH_LILYGO_T_POE_PRO 12
#define WLED_ETH_GLEDOPTO 13
#define WLED_ETH_QUINLED_V4_UNOQUAD 14
#define WLED_ETH_QUINLED_V4_OCTA 15
//Hue error codes
#define HUE_ERROR_INACTIVE 0
@@ -439,6 +473,32 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit");
#define ERR_OVERCURRENT 31 // An attached current sensor has measured a current above the threshold (not implemented)
#define ERR_UNDERVOLT 32 // An attached voltmeter has measured a voltage below the threshold (not implemented)
// JSON buffer lock owners
#define JSON_LOCK_UNKNOWN 255
#define JSON_LOCK_CFG_DES 1
#define JSON_LOCK_CFG_SER 2
#define JSON_LOCK_CFG_SEC_DES 3
#define JSON_LOCK_CFG_SEC_SER 4
#define JSON_LOCK_SETTINGS 5
#define JSON_LOCK_XML 6
#define JSON_LOCK_LEDMAP 7
// unused 8
#define JSON_LOCK_PRESET_LOAD 9
#define JSON_LOCK_PRESET_SAVE 10
#define JSON_LOCK_WS_RECEIVE 11
#define JSON_LOCK_WS_SEND 12
#define JSON_LOCK_IR 13
#define JSON_LOCK_SERVER 14
#define JSON_LOCK_MQTT 15
#define JSON_LOCK_SERIAL 16
#define JSON_LOCK_SERVEJSON 17
#define JSON_LOCK_NOTIFY 18
#define JSON_LOCK_PRESET_NAME 19
#define JSON_LOCK_LEDGAP 20
#define JSON_LOCK_LEDMAP_ENUM 21
#define JSON_LOCK_REMOTE 22
#define JSON_LOCK_OTA 23
// Timer mode types
#define NL_MODE_SET 0 //After nightlight time elapsed, set to target brightness
#define NL_MODE_FADE 1 //Fade to target brightness gradually
@@ -457,6 +517,8 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit");
#define SUBPAGE_UM 8
#define SUBPAGE_UPDATE 9
#define SUBPAGE_2D 10
#define SUBPAGE_PINS 11
#define SUBPAGE_LAST SUBPAGE_PINS
#define SUBPAGE_LOCK 251
#define SUBPAGE_PINREQ 252
#define SUBPAGE_CSS 253
@@ -472,23 +534,28 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit");
#define MAX_LEDS 1536 //can't rely on memory limit to limit this to 1536 LEDs
#elif defined(CONFIG_IDF_TARGET_ESP32S2)
#define MAX_LEDS 2048 //due to memory constraints S2
#elif defined(CONFIG_IDF_TARGET_ESP32C3)
#define MAX_LEDS 4096
#else
#define MAX_LEDS 16384
#endif
#endif
// maximum total memory that can be used for bus-buffers and pixel buffers
#ifndef MAX_LED_MEMORY
#ifdef ESP8266
#define MAX_LED_MEMORY 4096
#define MAX_LED_MEMORY (8*1024)
#else
#if defined(ARDUINO_ARCH_ESP32S2)
#define MAX_LED_MEMORY 16384
#elif defined(ARDUINO_ARCH_ESP32C3)
#define MAX_LED_MEMORY 32768
#if defined(CONFIG_IDF_TARGET_ESP32S2)
#ifndef BOARD_HAS_PSRAM
#define MAX_LED_MEMORY (28*1024) // S2 has ~170k of free heap after boot, using 28k is the absolute limit to keep WLED functional
#else
#define MAX_LED_MEMORY (48*1024) // with PSRAM there is more wiggle room as buffers get moved to PSRAM when needed (prioritize functionality over speed)
#endif
#elif defined(CONFIG_IDF_TARGET_ESP32S3)
#define MAX_LED_MEMORY (192*1024) // S3 has ~330k of free heap after boot
#elif defined(CONFIG_IDF_TARGET_ESP32C3)
#define MAX_LED_MEMORY (100*1024) // C3 has ~240k of free heap after boot, even with 8000 LEDs configured (2D) there is 30k of contiguous heap left
#else
#define MAX_LED_MEMORY 65536
#define MAX_LED_MEMORY (85*1024) // ESP32 has ~160k of free heap after boot and an additional 64k of 32bit access memory that is used for pixel buffers
#endif
#endif
#endif
@@ -553,7 +620,7 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit");
#ifdef ESP8266
#define JSON_BUFFER_SIZE 10240
#else
#if defined(ARDUINO_ARCH_ESP32S2)
#if defined(CONFIG_IDF_TARGET_ESP32S2)
#define JSON_BUFFER_SIZE 24576
#else
#define JSON_BUFFER_SIZE 32767
@@ -609,7 +676,12 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit");
#define DEFAULT_LED_PIN 2 // GPIO2 (D4) on Wemos D1 mini compatible boards, safe to use on any board
#endif
#else
#define DEFAULT_LED_PIN 16 // aligns with GPIO2 (D4) on Wemos D1 mini32 compatible boards (if it is unusable it will be reassigned in WS2812FX::finalizeInit())
#if defined(WLED_USE_ETHERNET)
#define DEFAULT_LED_PIN 4 // GPIO4 seems to be a "safe bet" for all known ethernet boards (issue #5155)
//#warning "Compiling with Ethernet support. The default LED pin has been changed to pin 4."
#else
#define DEFAULT_LED_PIN 16 // aligns with GPIO2 (D4) on Wemos D1 mini32 compatible boards (if it is unusable it will be reassigned in WS2812FX::finalizeInit())
#endif
#endif
#define DEFAULT_LED_TYPE TYPE_WS2812_RGB
#define DEFAULT_LED_COUNT 30
@@ -683,5 +755,6 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit");
#endif
#define WLED_O2_ATTR __attribute__((optimize("O2")))
#define WLED_O3_ATTR __attribute__((optimize("O3")))
#endif
+1 -1
View File
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta content='width=device-width' name='viewport'>
+204 -22
View File
@@ -51,6 +51,38 @@ function tooltip(cont=null) {
});
});
};
// sequential loading of external resources (JS or CSS) with retry, calls init() when done
function loadResources(files, init) {
let i = 0;
const loadNext = () => {
if (i >= files.length) {
if (init) {
d.documentElement.style.visibility = 'visible'; // make page visible after all files are loaded if it was hidden (prevent ugly display)
d.readyState === 'complete' ? init() : window.addEventListener('load', init);
}
return;
}
const file = files[i++];
const isCSS = file.endsWith('.css');
const el = d.createElement(isCSS ? 'link' : 'script');
if (isCSS) {
el.rel = 'stylesheet';
el.href = file;
const st = d.head.querySelector('style');
if (st) d.head.insertBefore(el, st); // insert before any <style> to allow overrides
else d.head.appendChild(el);
} else {
el.src = file;
d.head.appendChild(el);
}
el.onload = () => { loadNext(); };
el.onerror = () => {
i--; // load this file again
setTimeout(loadNext, 100);
};
};
loadNext();
}
// https://www.educative.io/edpresso/how-to-dynamically-load-a-js-file-in-javascript
function loadJS(FILE_URL, async = true, preGetV = undefined, postGetV = undefined) {
let scE = d.createElement("script");
@@ -58,7 +90,7 @@ function loadJS(FILE_URL, async = true, preGetV = undefined, postGetV = undefine
scE.setAttribute("type", "text/javascript");
scE.setAttribute("async", async);
d.body.appendChild(scE);
// success event
// success event
scE.addEventListener("load", () => {
//console.log("File loaded");
if (preGetV) preGetV();
@@ -94,6 +126,10 @@ function getLoc() {
}
}
function getURL(path) { return (loc ? locproto + "//" + locip : "") + path; }
// HTML entity escaper use on any remote/user-supplied text inserted into innerHTML
function esc(s) { return String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c])); }
// URL sanitizer blocks javascript: and data: URIs, use for externally supplied URLs for some basic safety
function safeUrl(u) { return /^https?:\/\//.test(u) ? u : '#'; }
function B() { window.open(getURL("/settings"),"_self"); }
var timeout;
function showToast(text, error = false) {
@@ -105,32 +141,43 @@ function showToast(text, error = false) {
x.style.animation = 'none';
timeout = setTimeout(function(){ x.className = x.className.replace("show", ""); }, 2900);
}
function uploadFile(fileObj, name) {
async function uploadFile(fileObj, name, callback) {
let file = fileObj.files?.[0]; // get first file, "?"" = optional chaining in case no file is selected
if (!file) { callback?.(false); return; }
if (/\.json$/i.test(name)) { // same as name.toLowerCase().endsWith('.json')
try {
const minified = JSON.stringify(JSON.parse(await file.text())); // validate and minify JSON
file = new Blob([minified], { type: file.type || "application/json" });
} catch (err) {
if (!confirm("JSON invalid. Continue?")) { callback?.(false); return; }
// proceed with original file if invalid but user confirms
}
}
var req = new XMLHttpRequest();
req.addEventListener('load', function(){showToast(this.responseText,this.status >= 400)});
req.addEventListener('error', function(e){showToast(e.stack,true);});
req.addEventListener('load', function(){showToast(this.responseText,this.status >= 400); if(callback) callback(this.status < 400);});
req.addEventListener('error', function(e){showToast("Upload failed",true); if(callback) callback(false);});
req.open("POST", "/upload");
var formData = new FormData();
formData.append("data", fileObj.files[0], name);
formData.append("data", file, name);
req.send(formData);
fileObj.value = '';
return false;
}
// connect to WebSocket, use parent WS or open new
// connect to WebSocket, use parent WS or open new, callback function gets passed the new WS object
function connectWs(onOpen) {
try {
if (top.window.ws && top.window.ws.readyState === WebSocket.OPEN) {
if (onOpen) onOpen();
return top.window.ws;
}
} catch (e) {}
getLoc(); // ensure globals (loc, locip, locproto) are up to date
let url = loc ? getURL('/ws').replace("http","ws") : "ws://"+window.location.hostname+"/ws";
let ws = new WebSocket(url);
ws.binaryType = "arraybuffer";
if (onOpen) { ws.onopen = onOpen; }
try { top.window.ws = ws; } catch (e) {} // store in parent for reuse
let ws;
try { ws = top.window.ws;} catch (e) {}
// reuse if open
if (ws && ws.readyState === WebSocket.OPEN) {
if (onOpen) onOpen(ws);
} else {
// create new ws connection
getLoc(); // ensure globals are up to date
let url = loc ? getURL('/ws').replace("http", "ws")
: "ws://" + window.location.hostname + "/ws";
ws = new WebSocket(url);
ws.binaryType = "arraybuffer";
if (onOpen) ws.onopen = () => onOpen(ws);
}
return ws;
}
@@ -153,8 +200,8 @@ function sendDDP(ws, start, len, colors) {
let pkt = new Uint8Array(11 + dLen); // DDP header is 10 bytes, plus 1 byte for WLED websocket protocol indicator
pkt[0] = 0x02; // DDP protocol indicator for WLED websocket. Note: below DDP protocol bytes are offset by 1
pkt[1] = 0x40; // flags: 0x40 = no push, 0x41 = push (i.e. render), note: this is DDP protocol byte 0
pkt[2] = 0x00; // reserved
pkt[3] = 0x01; // 1 = RGB (currently only supported mode)
pkt[2] = 0x00; // upper nibble is reserved, lower nibble is sequence number, if set to 0 no sequence checking is done (if enabled)
pkt[3] = 0x0B; // RGB, 8bit per channel
pkt[4] = 0x01; // destination id (not used but 0x01 is default output)
pkt[5] = (off >> 24) & 255; // DDP protocol 4-7 is offset
pkt[6] = (off >> 16) & 255;
@@ -175,3 +222,138 @@ function sendDDP(ws, start, len, colors) {
}
return true;
}
// Pin utilities
function getOwnerName(o,t,n) {
// Use firmware-provided name if available
if(n) return n;
if(!o) return "System"; // no owner provided
if(o===0x85){ return getBtnTypeName(t); } // button pin
return "UM #"+o;
}
function getBtnTypeName(t) {
var n=["None","Reserved","Push","Push Inv","Switch","PIR","Touch","Analog","Analog Inv","Touch Switch"];
var label = n[t] || "?";
return 'Button <span style="font-size:10px;color:#888">'+label+'</span>';
}
function getCaps(p,c) {
var r=[];
// Use touch info from settings endpoint
if(d.touch && d.touch.includes(p)) r.push("Touch");
if(d.ro_gpio && d.ro_gpio.includes(p)) r.push("Input Only");
// Use other caps from JSON (Analog, Boot, Input Only)
if(c&0x02) r.push("Analog");
if(c&0x08) r.push("Flash Boot");
if(c&0x10) r.push("Bootstrap");
return r.length?r.join(", "):"-";
}
// Fetch GPIO caps (/settings/s.js?p=11) then pin occupancy (/json/pins) with retry.
// Caches result in d.pinsData. Calls cb() when ready (or on failure).
// If page already loaded its own s.js (d.max_gpio set), skips caps load and goes straight to pins fetch.
function fetchPinInfo(cb, retries=5) {
if (d.pinsData) { cb&&cb(); return; }
var done=false, fr=retries;
function doFetch() {
fetch(getURL('/json/pins'))
.then(r=>r.json())
.then(j=>{ if(!done){done=true; d.pinsData=j.pins||[]; cb&&cb();} })
.catch(()=>{ fr-->0 ? setTimeout(doFetch,100) : (!done&&(done=true,d.pinsData=[],cb&&cb())); });
}
if (d.max_gpio) { doFetch(); return; }
// Load GPIO caps from s.js?p=11 first (sets d.rsvd/ro_gpio/max_gpio/touch/adc/um_p)
d.max_gpio=50; d.rsvd=[]; d.ro_gpio=[]; d.touch=[]; d.adc=[]; d.um_p=[];
var cr=retries;
function tryCaps() {
var s=cE("script"); s.src=getURL('/settings/s.js?p=11');
d.body.appendChild(s);
s.onload=function(){ GetV(); doFetch(); };
s.onerror=function(){ cr-->0 ? setTimeout(tryCaps,100) : doFetch(); };
}
tryCaps();
}
// Pin dropdown utilities
// Create or rebuild a pin <select> from an <input> or existing <select>
// name: form field name, requirement flags bitmask: 1=output, 2=touch, 4=ADC
function makePinSelect(name, flags) {
let el = gN(name);
if (!el) return null;
let v = parseInt(el.value);
if (isNaN(v)) v = -1;
let sel;
if (el.tagName === "SELECT") {
sel = el;
while (sel.lastChild) sel.lastChild.remove();
} else {
sel = cE('select');
sel.classList.add("pin");
sel.name = el.name;
if (el.required) sel.required = true;
let oc = el.getAttribute("onchange");
if (oc) sel.setAttribute("onchange", oc);
el.parentElement.replaceChild(sel, el);
}
let hasV = false;
for (let j = -1; j < (d.max_gpio||0); j++) {
if (j > -1 && d.rsvd && d.rsvd.includes(j)) continue;
if (j > -1 && (flags & 1) && d.ro_gpio && d.ro_gpio.includes(j)) continue;
if (j > -1 && (flags & 2) && (!d.touch || !d.touch.includes(j))) continue;
if (j > -1 && (flags & 4) && (!d.adc || !d.adc.includes(j))) continue;
let pInfo = d.pinsData && d.pinsData.find(p => p.p === j);
let used = j > -1 && pInfo && pInfo.a && j !== v;
let txt = j === -1 ? "unused" : `${j}`;
if (used) txt += ` (${getOwnerName(pInfo.o, pInfo.t, pInfo.n)})`;
// if (j > -1 && d.ro_gpio && d.ro_gpio.includes(j)) txt += " (R/O)"; // read only pins note: removed as pin is not shown for outputs
let opt = cE("option");
opt.value = j;
opt.text = txt;
sel.appendChild(opt);
if (j === v) { opt.selected = true; hasV = true; }
else if (used) opt.disabled = true;
}
// Safety for invalid pins currently saved
if (!hasV && v >= 0) {
let opt = cE("option");
opt.value = v; opt.text = v + " ⚠"; opt.selected = true;
sel.insertBefore(opt, sel.options[1]);
}
sel.dataset.val = v;
return sel;
}
// Convert pin <select> back to <input type="number">
function unmakePinSelect(name) {
let sel = gN(name);
if (!sel || sel.tagName !== "SELECT") return null;
let inp = cE('input');
inp.type = "number";
inp.name = sel.name;
inp.value = sel.value;
inp.className = "s";
if (sel.required) inp.required = true;
let oc = sel.getAttribute("onchange");
if (oc) inp.setAttribute("onchange", oc);
sel.parentElement.replaceChild(inp, sel);
return inp;
}
// Add option to select, auto-select matching data-val
function addOption(sel, txt, val) {
if (!sel) return null;
let opt = cE("option");
opt.value = val;
opt.text = txt;
sel.appendChild(opt);
if (sel.dataset.val !== undefined) {
for (let i = 0; i < sel.options.length; i++) {
if (sel.options[i].value == sel.dataset.val) { sel.selectedIndex = i; break; }
}
}
return opt;
}
+886 -641
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html><head><meta content='width=device-width' name='viewport'>
<html lang="en"><head><meta content='width=device-width' name='viewport'>
<title>DMX Map</title>
<script>function B(){window.history.back()};function RS(){window.location = "/settings";}function RP(){top.location.href="/";}function FM() {
var dmxlabels = ["SET 0","RED","GREEN","BLUE","WHITE","SHUTTER","SET 255", "DISABLED"];
+42 -25
View File
@@ -5,12 +5,6 @@
<meta name="author" content="DedeHai, based on editor by Me-No-Dev">
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport"><!-- prevent too much scaling on mobile -->
<link rel="shortcut icon" href="favicon.ico">
<link href="style.css" rel="stylesheet">
<!-- Optional lightweight JSON editor - fallback to textarea if CDN is unavailable -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.23.4/ace.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.23.4/mode-json.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.23.4/theme-monokai.min.js"></script>
<script src="common.js"></script>
<style>
/* Editor-specific styles */
body {
@@ -113,6 +107,34 @@ body {
@keyframes spin { to { transform: rotate(360deg); } }
</style>
<script>
// load common.js with retry on error, then load Ace editor from CDN with retries, then load style.css
function loadFiles(u, r, cb) {
var s = document.createElement('script'),
// set 1s timeout if retry count is less than 10 (i.e. for Ace scripts)
tm = r < 10 ? setTimeout(() => { s.onload=s.onerror=null; cb(); }, 1000) : 0;
s.src = u;
s.onload = () => { clearTimeout(tm); cb(); };
s.onerror = () => {
clearTimeout(tm);
if (r !== 0) setTimeout(() => loadFiles(u, r < 0 ? r : r - 1, cb), 100); // if r is -1 or > 0 try again
else cb();
};
document.head.appendChild(s);
}
loadFiles('common.js', -1, () => {
const cdn = "https://cdnjs.cloudflare.com/ajax/libs/ace/1.23.4/";
// load external scripts in sequence (3 retries each)
loadFiles(cdn + 'ace.min.js', 3, () => {
loadFiles(cdn + 'mode-json.min.js', 3, () => {
loadFiles(cdn + 'theme-monokai.min.js', 3, () => {
// all scripts loaded (or skipped) load style.css
if (window.loadResources) loadResources(['style.css'], S);
});
});
});
});
var QueuedRequester = function(){ this.q=[]; this.r=false; this.x=null; }
QueuedRequester.prototype = {
_request: function(req){
@@ -411,7 +433,7 @@ function createEditor(element,file){
// Check filename from text field or current file
var pathField = gId("filepath");
var filename = (pathField && pathField.value) ? pathField.value : currentFile;
aceEditor.session.setMode(filename && filename.toLowerCase().endsWith('.json') ? "ace/mode/json" : "ace/mode/text");
aceEditor.session.setMode(filename && (/\.json$/i.test(filename)) ? "ace/mode/json" : "ace/mode/text"); // same as filename.toLowerCase().endsWith('.json')
}
// Try to initialize Ace editor if available
@@ -467,7 +489,7 @@ function createEditor(element,file){
var filename = pathField ? pathField.value : currentFile;
var border = "2px solid #333";
if (filename && filename.toLowerCase().endsWith('.json')) {
if (filename && (/\.json$/i.test(filename))) { // same as filename.toLowerCase().endsWith('.json')
try {
JSON.parse(ta.value);
} catch(e) {
@@ -478,23 +500,19 @@ function createEditor(element,file){
};
function saveFile(filename,data){
var finalData = data;
// Minify JSON files before upload
if (filename.toLowerCase().endsWith('.json')) {
var outdata = data;
if (/\.json$/i.test(filename)) { // same as filename.toLowerCase().endsWith('.json')
try {
finalData = JSON.stringify(JSON.parse(data));
outdata = JSON.stringify(JSON.parse(data)); // validate and minify
} catch(e) {
alert("Invalid JSON! Please fix syntax.");
alert("Invalid JSON! Please fix.");
return;
}
}
var fd=new FormData();
fd.append("file",new Blob([finalData],{type:"text/plain"}),filename);
req.add("POST","/upload",fd,function(st,resp){
if (st!=200) alert("ERROR "+st+": "+resp);
else {
showToast("File saved");
uploadFile({files: [new Blob([outdata], {type:"text/plain"})]}, filename, function(s) {
if(s) {
refreshTree();
loadFile(filename); // (re)load if saved successfully to update formating or show file content
}
});
}
@@ -505,9 +523,9 @@ function createEditor(element,file){
gId("preview").style.display="none";
gId("editor").style.display="flex";
if (st==200) {
if (filename.toLowerCase().endsWith('.json')) {
if ((/\.json$/i.test(filename))) { // same as filename.toLowerCase().endsWith('.json')
try {
setContent(filename.toLowerCase().includes('ledmap') ? prettyLedmap(resp) : JSON.stringify(JSON.parse(resp), null, 2));
setContent(/ledmap/i.test(filename) ? prettyLedmap(resp) : JSON.stringify(JSON.parse(resp), null, 2)); // pretty-print ledmap files (i.e. if file name includes "ledmap" case-insensitive)
} catch(e) {
setContent(resp);
}
@@ -534,8 +552,7 @@ function createEditor(element,file){
}
if (!fn.startsWith("/")) fn = "/" + fn;
currentFile = fn; // Update current file
saveFile(fn, getContent());
loadFile(fn);
saveFile(fn, getContent())
},
loadText:function(fn){
currentFile=fn;
@@ -558,7 +575,7 @@ function createEditor(element,file){
};
}
function onBodyLoad(){
function S(){
var vars={};
window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi,function(m,k,v){
vars[decodeURIComponent(k)]=decodeURIComponent(v);
@@ -577,7 +594,7 @@ function onBodyLoad(){
}
</script>
</head>
<body onload="onBodyLoad()">
<body>
<div id="toast"></div>
<div id="loader"><div class="loader"></div></div>
<div id="top"></div>
@@ -0,0 +1,14 @@
To edit the current font, this is the workflow:
go to https://icomoon.io/
In the menu, go to manage projects and import the json file from this folder and load it
Add new icons or exchange existing ones: if changing existing one, make sure the unicode stays the same (can be edited before exporting)
Go to "Generate SVG & More" and check the size of new icons (clicking on icons brings up the editor) -> scale new icons to match the size of existing ones
Go to "Generate font" tab, check unicodes are correct (can use any unicode, range > e900 is "custom range" and now preferred)
Download the font package and replace the files in this folder with new files
Using an online converter, convert the *.woff font into woff2 format (about half the file size)
Using another online converter, convert the woff2 font to base64 encoding for CSS
in index.css, replace the font string at the top, keep the "data:font/woff2;charset=utf-8;" and dont use octet-stream (browser compatibility).
enjoy your new icons in the UI :)
+1 -1
View File
@@ -1,6 +1,6 @@
Open *demo.html* to see a list of all the glyphs in your font along with their codes/ligatures.
To use the generated font in desktop programs, you can install the TTF font. In order to copy the character associated with each icon, refer to the text box at the bottom right corner of each glyph in demo.html. The character inside this text box may be invisible; but it can still be copied. See this guide for more info: https://icomoon.io/#docs/local-fonts
To use the generated font in desktop programs, you can install the TTF font. In order to copy the character associated with each icon, refer to the text box at the bottom right corner of each glyph in demo.html. The character inside this text box may be invisible; but it can still be copied. See this guide for more info: https://icomoon.io/docs#install
You won't need any of the files located under the *demo-files* directory when including the generated font in your own projects.
+6
View File
@@ -149,4 +149,10 @@ p {
.fs1 {
font-size: 32px;
}
.fs2 {
font-size: 28px;
}
.fs3 {
font-size: 32px;
}
+72 -24
View File
@@ -9,11 +9,59 @@
<link rel="stylesheet" href="style.css"></head>
<body>
<div class="bgc1 clearfix">
<h1 class="mhmm mvm"><span class="fgc1">Font Name:</span> wled122 <small class="fgc1">(Glyphs:&nbsp;23)</small></h1>
<h1 class="mhmm mvm"><span class="fgc1">Font Name:</span> wled122 <small class="fgc1">(Glyphs:&nbsp;26)</small></h1>
</div>
<div class="clearfix mhl ptl">
<h1 class="mvm mtn fgc1">Grid Size: 16</h1>
<div class="glyph fs1">
<div class="clearfix bshadow0 pbs">
<span class="i-search"></span>
<span class="mls"> i-search</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="e0a1" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe0a1;" class="unitRight size1of2 talign-right" />
</fieldset>
<div class="fs0 bshadow0 clearfix hidden-true">
<span class="unit pvs fgc1">liga: </span>
<input type="text" readonly value="search, magnifier" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="clearfix bshadow0 pbs">
<span class="i-pixelforge"></span>
<span class="mls"> i-pixelforge</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="e900" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe900;" class="unitRight size1of2 talign-right" />
</fieldset>
<div class="fs0 bshadow0 clearfix hidden-true">
<span class="unit pvs fgc1">liga: </span>
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
</div>
<div class="clearfix mhl ptl">
<h1 class="mvm mtn fgc1">Grid Size: 14</h1>
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs">
<span class="i-editor"></span>
<span class="mls"> i-editor</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="e901" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe901;" class="unitRight size1of2 talign-right" />
</fieldset>
<div class="fs0 bshadow0 clearfix hidden-true">
<span class="unit pvs fgc1">liga: </span>
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
</div>
<div class="clearfix mhl ptl">
<h1 class="mvm mtn fgc1">Grid Size: Unknown</h1>
<div class="glyph fs1">
<div class="glyph fs3">
<div class="clearfix bshadow0 pbs">
<span class="i-pattern"></span>
<span class="mls"> i-pattern</span>
@@ -27,7 +75,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs3">
<div class="clearfix bshadow0 pbs">
<span class="i-segments"></span>
<span class="mls"> i-segments</span>
@@ -41,7 +89,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs3">
<div class="clearfix bshadow0 pbs">
<span class="i-sun"></span>
<span class="mls"> i-sun</span>
@@ -55,7 +103,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs3">
<div class="clearfix bshadow0 pbs">
<span class="i-palette"></span>
<span class="mls"> i-palette</span>
@@ -69,7 +117,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs3">
<div class="clearfix bshadow0 pbs">
<span class="i-eye"></span>
<span class="mls"> i-eye</span>
@@ -83,7 +131,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs3">
<div class="clearfix bshadow0 pbs">
<span class="i-speed"></span>
<span class="mls"> i-speed</span>
@@ -97,7 +145,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs3">
<div class="clearfix bshadow0 pbs">
<span class="i-expand"></span>
<span class="mls"> i-expand</span>
@@ -111,7 +159,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs3">
<div class="clearfix bshadow0 pbs">
<span class="i-power"></span>
<span class="mls"> i-power</span>
@@ -125,7 +173,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs3">
<div class="clearfix bshadow0 pbs">
<span class="i-settings"></span>
<span class="mls"> i-settings</span>
@@ -139,7 +187,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs3">
<div class="clearfix bshadow0 pbs">
<span class="i-playlist"></span>
<span class="mls"> i-playlist</span>
@@ -153,7 +201,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs3">
<div class="clearfix bshadow0 pbs">
<span class="i-night"></span>
<span class="mls"> i-night</span>
@@ -167,7 +215,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs3">
<div class="clearfix bshadow0 pbs">
<span class="i-cancel"></span>
<span class="mls"> i-cancel</span>
@@ -181,7 +229,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs3">
<div class="clearfix bshadow0 pbs">
<span class="i-sync"></span>
<span class="mls"> i-sync</span>
@@ -195,7 +243,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs3">
<div class="clearfix bshadow0 pbs">
<span class="i-confirm"></span>
<span class="mls"> i-confirm</span>
@@ -209,7 +257,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs3">
<div class="clearfix bshadow0 pbs">
<span class="i-brightness"></span>
<span class="mls"> i-brightness</span>
@@ -223,7 +271,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs3">
<div class="clearfix bshadow0 pbs">
<span class="i-nodes"></span>
<span class="mls"> i-nodes</span>
@@ -237,7 +285,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs3">
<div class="clearfix bshadow0 pbs">
<span class="i-add"></span>
<span class="mls"> i-add</span>
@@ -251,7 +299,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs3">
<div class="clearfix bshadow0 pbs">
<span class="i-edit"></span>
<span class="mls"> i-edit</span>
@@ -265,7 +313,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs3">
<div class="clearfix bshadow0 pbs">
<span class="i-intensity"></span>
<span class="mls"> i-intensity</span>
@@ -279,7 +327,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs3">
<div class="clearfix bshadow0 pbs">
<span class="i-star"></span>
<span class="mls"> i-star</span>
@@ -293,7 +341,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs3">
<div class="clearfix bshadow0 pbs">
<span class="i-info"></span>
<span class="mls"> i-info</span>
@@ -307,7 +355,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs3">
<div class="clearfix bshadow0 pbs">
<span class="i-del"></span>
<span class="mls"> i-del</span>
@@ -321,7 +369,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs3">
<div class="clearfix bshadow0 pbs">
<span class="i-presets"></span>
<span class="mls"> i-presets</span>
+36
View File
@@ -0,0 +1,36 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
<svg xmlns="http://www.w3.org/2000/svg">
<metadata>Generated by IcoMoon</metadata>
<defs>
<font id="wled122" horiz-adv-x="1024">
<font-face units-per-em="1024" ascent="819.2" descent="-204.8" />
<missing-glyph horiz-adv-x="1024" />
<glyph unicode="&#x20;" horiz-adv-x="0" d="" />
<glyph unicode="&#xe037;" glyph-name="del" d="M256 21.333v512h512v-512c0-46.933-38.4-85.333-85.333-85.333h-341.334c-46.933 0-85.333 38.4-85.333 85.333zM810.667 661.333v-85.333h-597.334v85.333h149.334l42.666 42.667h213.334l42.666-42.667h149.334z" />
<glyph unicode="&#xe04c;" glyph-name="presets" d="M704 704c131.413 0 234.667-103.253 234.667-234.667 0-161.28-145.067-292.693-364.8-491.946l-61.867-56.32-61.867 55.893c-219.733 199.68-364.8 331.093-364.8 492.373 0 131.414 103.254 234.667 234.667 234.667 74.24 0 145.493-34.56 192-89.173 46.507 54.613 117.76 89.173 192 89.173zM516.267 40.533c203.093 183.894 337.066 305.494 337.066 428.8 0 85.334-64 149.334-149.333 149.334-65.707 0-129.707-42.24-151.893-100.694h-79.787c-22.613 58.454-86.613 100.694-152.32 100.694-85.333 0-149.333-64-149.333-149.334 0-123.306 133.973-244.906 337.066-428.8l4.267-4.266z" />
<glyph unicode="&#xe066;" glyph-name="info" d="M512 746.667c235.52 0 426.667-191.147 426.667-426.667s-191.147-426.667-426.667-426.667-426.667 191.147-426.667 426.667 191.147 426.667 426.667 426.667zM554.667 106.667v256h-85.334v-256h85.334zM554.667 448v85.333h-85.334v-85.333h85.334z" />
<glyph unicode="&#xe08f;" glyph-name="power" d="M554.667 704v-426.667h-85.334v426.667h85.334zM760.747 611.413c82.773-70.4 135.253-174.506 135.253-291.413 0-212.053-171.947-384-384-384s-384 171.947-384 384c0 116.907 52.48 221.013 135.253 291.413l60.16-60.16c-66.986-54.613-110.080-137.813-110.080-231.253 0-165.12 133.547-298.667 298.667-298.667s298.667 133.547 298.667 298.667c0 93.44-43.094 176.64-110.507 230.827z" />
<glyph unicode="&#xe0a1;" glyph-name="search" d="M902.213 27.99l-197.073 167.615c-20.373 18.336-42.161 26.752-59.761 25.941 46.52 54.492 74.622 125.188 74.622 202.455 0 172.313-139.687 312-312 312-172.311 0-312-139.687-312-312s139.687-312 312-312c77.266 0 147.962 28.1 202.455 74.624-0.812-17.6 7.605-39.388 25.941-59.761l167.615-197.073c28.698-31.887 75.58-34.574 104.178-5.976s25.913 75.48-5.974 104.178zM408 216c-114.874 0-208 93.125-208 208s93.125 208 208 208 208-93.125 208-208-93.125-208-208-208z" />
<glyph unicode="&#xe0a2;" glyph-name="settings" d="M816.64 280.064l85.504-67.584c8.192-6.144 10.24-16.896 5.12-26.112l-81.92-141.824c-5.12-9.216-15.872-12.8-25.088-9.216l-101.888 40.96c-20.992-15.872-44.032-29.696-69.12-39.936l-15.36-108.544c-1.024-10.24-9.728-17.408-19.968-17.408h-163.84c-10.24 0-18.432 7.168-20.48 17.408l-15.36 108.544c-25.088 10.24-47.616 23.552-69.12 39.936l-101.888-40.96c-9.216-3.072-19.968 0-25.088 9.216l-81.92 141.824c-4.608 8.704-2.56 19.968 5.12 26.112l86.528 67.584c-2.048 12.8-3.072 26.624-3.072 39.936s1.536 27.136 3.584 39.936l-86.528 67.584c-8.192 6.144-10.24 16.896-5.12 26.112l81.92 141.824c5.12 9.216 15.872 12.8 25.088 9.216l101.888-40.96c20.992 15.872 44.032 29.696 69.12 39.936l15.36 108.544c1.536 10.24 9.728 17.408 19.968 17.408h163.84c10.24 0 18.944-7.168 20.48-17.408l15.36-108.544c25.088-10.24 47.616-23.552 69.12-39.936l101.888 40.96c9.216 3.072 19.968 0 25.088-9.216l81.92-141.824c4.608-8.704 2.56-19.968-5.12-26.112l-86.528-67.584c2.048-12.8 3.072-26.112 3.072-39.936s-1.024-27.136-2.56-39.936zM512 166.4c84.48 0 153.6 69.12 153.6 153.6s-69.12 153.6-153.6 153.6-153.6-69.12-153.6-153.6 69.12-153.6 153.6-153.6z" />
<glyph unicode="&#xe0e8;" glyph-name="eye" d="M512 640c213.333 0 395.52-132.693 469.333-320-73.813-187.307-256-320-469.333-320s-395.52 132.693-469.333 320c73.813 187.307 256 320 469.333 320zM512 106.667c117.76 0 213.333 95.573 213.333 213.333s-95.573 213.333-213.333 213.333-213.333-95.573-213.333-213.333 95.573-213.333 213.333-213.333zM512 448c70.827 0 128-57.173 128-128s-57.173-128-128-128-128 57.173-128 128 57.173 128 128 128z" />
<glyph unicode="&#xe116;" glyph-name="sync" d="M512 661.333c188.587 0 341.333-152.746 341.333-341.333 0-66.987-19.626-129.28-52.906-181.76l-62.294 62.293c19.2 35.414 29.867 76.374 29.867 119.467 0 141.227-114.773 256-256 256v-128l-170.667 170.667 170.667 170.666v-128zM512 64v128l170.667-170.667-170.667-170.666v128c-188.587 0-341.333 152.746-341.333 341.333 0 66.987 19.626 129.28 52.906 181.76l62.294-62.293c-19.2-35.414-29.867-76.374-29.867-119.467 0-141.227 114.773-256 256-256z" />
<glyph unicode="&#xe139;" glyph-name="playlist" d="M556.8 414.293l125.867-94.293-256-192v384zM556.8 414.293l125.867-94.293-256-192v384zM556.8 414.293l-130.133 97.707v-384l256 192zM469.333 658.347c-62.293-7.68-119.040-32.427-166.4-69.12l-60.586 61.013c63.146 51.627 141.226 85.76 226.986 94.293v-86.186zM242.773 529.067c-36.693-47.36-61.44-104.107-69.12-166.4h-86.186c8.533 85.76 42.666 163.84 94.293 226.986zM173.653 277.333c7.68-62.293 32.427-119.040 69.12-165.973l-61.013-61.013c-51.627 63.146-85.76 141.226-94.293 226.986h86.186zM242.347-10.24l60.586 61.013c47.36-36.693 104.107-61.44 166.4-69.12v-86.186c-85.333 8.533-163.84 42.666-226.986 94.293zM938.667 320c0-220.16-167.254-401.92-381.867-424.533v86.186c167.253 22.187 296.533 165.547 296.533 338.347s-129.28 316.16-296.533 338.347v86.186c214.613-22.613 381.867-204.373 381.867-424.533z" />
<glyph unicode="&#xe18a;" glyph-name="add" d="M810.667 277.333h-256v-256h-85.334v256h-256v85.334h256v256h85.334v-256h256v-85.334z" />
<glyph unicode="&#xe22d;" glyph-name="nodes" d="M85.333 106.667v42.666h128v-170.666h-128v42.666h85.334v21.334h-42.667v42.666h42.667v21.334h-85.334zM128 490.667v128h-42.667v42.666h85.334v-170.666h-42.667zM85.333 362.667v42.666h128v-38.4l-76.8-89.6h76.8v-42.666h-128v38.4l76.8 89.6h-76.8zM298.667 618.667h597.333v-85.334h-597.333v85.334zM298.667 21.333v85.334h597.333v-85.334h-597.333zM298.667 277.333v85.334h597.333v-85.334h-597.333z" />
<glyph unicode="&#xe23d;" glyph-name="pattern" d="M511.573 746.667c235.947 0 427.094-191.147 427.094-426.667s-191.147-426.667-427.094-426.667c-235.52 0-426.24 191.147-426.24 426.667s190.72 426.667 426.24 426.667zM512-21.333c188.587 0 341.333 152.746 341.333 341.333s-152.746 341.333-341.333 341.333-341.333-152.746-341.333-341.333 152.746-341.333 341.333-341.333zM661.333 362.667c-35.413 0-64 28.586-64 64s28.587 64 64 64c35.414 0 64-28.587 64-64s-28.586-64-64-64zM362.667 362.667c-35.414 0-64 28.586-64 64s28.586 64 64 64c35.413 0 64-28.587 64-64s-28.587-64-64-64zM512 85.333c-99.413 0-183.893 62.294-218.027 149.334h436.054c-34.134-87.040-118.614-149.334-218.027-149.334z" />
<glyph unicode="&#xe2a2;" glyph-name="night" d="M386.4 738.667c231.104 0 418.667-187.563 418.667-418.667s-187.563-418.667-418.667-418.667c-43.96 0-85.827 6.699-125.6 19.259 169.979 53.171 293.067 211.845 293.067 399.408s-123.088 346.237-293.067 399.408c39.773 12.56 81.64 19.259 125.6 19.259z" />
<glyph unicode="&#xe2a6;" glyph-name="brightness" d="M853.333 461.227l141.227-141.227-141.227-141.227v-200.106h-200.106l-141.227-141.227-141.227 141.227h-200.106v200.106l-141.227 141.227 141.227 141.227v200.106h200.106l141.227 141.227 141.227-141.227h200.106v-200.106zM512 64c141.227 0 256 114.773 256 256s-114.773 256-256 256-256-114.773-256-256 114.773-256 256-256zM512 490.667c94.293 0 170.667-76.374 170.667-170.667s-76.374-170.667-170.667-170.667-170.667 76.374-170.667 170.667 76.374 170.667 170.667 170.667z" />
<glyph unicode="&#xe2b3;" glyph-name="palette" d="M512 704c212.053 0 384-152.747 384-341.333 0-117.76-95.573-213.334-213.333-213.334h-75.52c-35.414 0-64-28.586-64-64 0-16.213 6.4-31.146 16.213-42.24 10.24-11.52 16.64-26.453 16.64-43.093 0-35.413-28.587-64-64-64-212.053 0-384 171.947-384 384s171.947 384 384 384zM277.333 320c35.414 0 64 28.587 64 64s-28.586 64-64 64c-35.413 0-64-28.587-64-64s28.587-64 64-64zM405.333 490.667c35.414 0 64 28.586 64 64s-28.586 64-64 64c-35.413 0-64-28.587-64-64s28.587-64 64-64zM618.667 490.667c35.413 0 64 28.586 64 64s-28.587 64-64 64c-35.414 0-64-28.587-64-64s28.586-64 64-64zM746.667 320c35.413 0 64 28.587 64 64s-28.587 64-64 64c-35.414 0-64-28.587-64-64s28.586-64 64-64z" />
<glyph unicode="&#xe2c6;" glyph-name="edit" d="M128 96l471.893 471.893 160-160-471.893-471.893h-160v160zM883.627 531.627l-78.080-78.080-160 160 78.080 78.080c16.64 16.64 43.52 16.64 60.16 0l99.84-99.84c16.64-16.64 16.64-43.52 0-60.16z" />
<glyph unicode="&#xe325;" glyph-name="speed" d="M640 789.333v-85.333h-256v85.333h256zM469.333 234.667v256h85.334v-256h-85.334zM811.947 516.693c52.48-65.706 84.053-148.906 84.053-239.36 0-212.053-171.52-384-384-384s-384 171.947-384 384c0 212.054 171.947 384 384 384 90.453 0 173.653-31.573 239.787-84.48l60.586 60.587c21.76-17.92 41.814-38.4 60.16-60.16zM512-21.333c165.12 0 298.667 133.546 298.667 298.666s-133.547 298.667-298.667 298.667-298.667-133.547-298.667-298.667 133.547-298.666 298.667-298.666z" />
<glyph unicode="&#xe333;" glyph-name="sun" d="M288.427 625.493l-60.587-60.16-76.373 76.374 60.16 60.16zM170.667 384v-85.333h-128v85.333h128zM554.667 808.533v-125.866h-85.334v125.866h85.334zM872.533 641.707l-76.373-76.374-60.16 60.16 76.373 76.374zM735.573 57.173l59.734 59.734 76.8-76.374-60.16-60.16zM853.333 384h128v-85.333h-128v85.333zM512 597.333c141.227 0 256-114.773 256-256s-114.773-256-256-256-256 114.774-256 256c0 141.227 114.773 256 256 256zM469.333-125.867v125.867h85.334v-125.867h-85.334zM151.467 40.96l76.373 76.8 60.16-60.16-76.373-76.8z" />
<glyph unicode="&#xe34b;" glyph-name="segments" d="M511.573 40.96l314.88 244.907 69.547-54.187-384-298.667-384 298.667 69.12 53.76zM512 149.333l-384 298.667 384 298.667 384-298.667-69.973-54.187z" />
<glyph unicode="&#xe38f;" glyph-name="cancel" d="M512 746.667c235.947 0 426.667-190.72 426.667-426.667s-190.72-426.667-426.667-426.667-426.667 190.72-426.667 426.667 190.72 426.667 426.667 426.667zM725.333 166.827l-153.173 153.173 153.173 153.173-60.16 60.16-153.173-153.173-153.173 153.173-60.16-60.16 153.173-153.173-153.173-153.173 60.16-60.16 153.173 153.173 153.173-153.173z" />
<glyph unicode="&#xe390;" glyph-name="confirm" d="M384 142.080l451.84 451.413 60.16-60.16-512-512-238.507 238.507 60.587 60.16z" />
<glyph unicode="&#xe395;" glyph-name="expand" d="M707.84 465.493l60.16-60.16-256-256-256 256 60.16 60.16 195.84-195.413z" />
<glyph unicode="&#xe409;" glyph-name="intensity" d="M576 803.413c166.827-133.546 277.333-338.773 277.333-568.746 0-188.587-152.746-341.334-341.333-341.334s-341.333 152.747-341.333 341.334c0 144.213 51.626 276.906 137.813 379.306l-1.28-15.36c0-87.893 66.56-159.146 154.88-159.146 87.893 0 145.493 71.253 145.493 159.146 0 91.734-31.573 204.8-31.573 204.8zM499.627 21.333c113.066 0 204.8 91.734 204.8 204.8 0 59.307-8.534 117.334-25.174 172.374-43.52-58.454-121.6-94.72-197.12-110.080-75.093-15.36-119.893-64-119.893-133.12 0-74.24 61.44-133.974 137.387-133.974z" />
<glyph unicode="&#xe410;" glyph-name="star" d="M938.667 437.76l-232.534-201.813 69.547-299.947-263.68 159.147-263.68-159.147 69.973 299.947-232.96 201.813 306.774 26.027 119.893 282.88 119.893-282.454zM512 174.933l160.853-97.28-42.666 182.614 141.653 122.88-186.88 16.213-72.96 172.373-72.533-171.946-186.88-16.214 141.653-122.88-42.667-182.613z" />
<glyph unicode="&#xe900;" glyph-name="pixelforge" d="M910.398 66.419l-241.236 241.236c-14.934 14.934-39.371 14.934-54.306 0l-18.102-18.102-147.2 147.2 241.646 241.648h-256.001l-113.645-113.645-11.249 11.247h-54.306v-54.306l11.247-11.247-164.848-164.849 127.999-127.999 164.848 164.848 147.2-147.2-18.102-18.102c-14.934-14.934-14.934-39.371 0-54.306l241.236-241.236c14.934-14.934 39.371-14.934 54.306 0l90.509 90.509c14.935 14.934 14.935 39.371 0.002 54.306z" />
<glyph unicode="&#xe901;" glyph-name="editor" horiz-adv-x="1074" d="M976.272 266.38c0-11.223-7.016-22.448-14.5-30.867l-157.14-185.202c-27.126-31.802-82.311-57.055-123.469-57.055h-508.837c-16.837 0-40.688 5.146-40.688 26.191 0 11.223 7.016 22.448 14.5 30.867l157.14 185.202c27.126 31.802 82.311 57.055 123.469 57.055h508.837c16.837 0 40.688-5.146 40.688-26.191zM815.856 427.264v-74.828h-389.112c-58.461 0-130.952-33.208-168.835-78.104l-159.949-188.009c0 3.74-0.467 7.951-0.467 11.691v448.977c0 57.523 47.233 104.761 104.761 104.761h149.66c57.523 0 104.761-47.233 104.761-104.761v-14.968h254.418c57.523 0 104.761-47.233 104.761-104.761z" />
</font></defs></svg>

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
+12 -4
View File
@@ -1,10 +1,9 @@
@font-face {
font-family: 'wled122';
src:
url('fonts/wled122.woff2?e3eban') format('woff2'),
url('fonts/wled122.ttf?e3eban') format('truetype'),
url('fonts/wled122.woff?e3eban') format('woff'),
url('fonts/wled122.svg?e3eban#wled122') format('svg');
url('fonts/wled122.ttf?2tjc6') format('truetype'),
url('fonts/wled122.woff?2tjc6') format('woff'),
url('fonts/wled122.svg?2tjc6#wled122') format('svg');
font-weight: normal;
font-style: normal;
font-display: block;
@@ -25,6 +24,15 @@
-moz-osx-font-smoothing: grayscale;
}
.i-search:before {
content: "\e0a1";
}
.i-pixelforge:before {
content: "\e900";
}
.i-editor:before {
content: "\e901";
}
.i-pattern:before {
content: "\e23d";
}
+8 -3
View File
@@ -1,6 +1,6 @@
@font-face {
font-family: "WIcons";
src: url(data:font/woff2;charset=utf-8;base64,d09GMgABAAAAAAnUAAsAAAAAE1AAAAmFAAGZmgAAAAAAAAAAAAAAAAAAAAAAAAAABmAAgXwRCAqcYJZIATYCJANwCzoABCAFgwYHIBs7D8iOwzgm3MXMnzZCktnjcbN+QlJLaJ3ulULplpW6UqWioeS91Jye0jUlJwZr5nTdE3LntdPvAg+ft/fbsLsGlNLuhlmQjKi7NPDEIgwTmP//a6mdl+SHUBhEIdHFxak7s4E/yzhJSjC7BQQLfDwopF/i6aqSElEFDXx8ZVWjy3rym4N6FlZQ4hu+nXsGIDMQF3gAxa14AgArtVMhfkgjfEAbiChwuSIwEUCmudPhiQdT6rvIjLSRZEwDhF9BIsooI53TIRIoIUD8kyNZI7UjAyMrR/aM/DwaOpozah9LGCsY2zN2YOzs2L3xqeNp4zXjq8bXT/hMBLj/53YDAIS+7u668n3H+HRPdZd1u3TzdRZdVMTfIl5HfKgd1b7Svqd9W9uprdP8QTOmeaz5TPORJlDDjHVjG0ANMQYsmRrKlmpyqV7kubIQC2GSIkFS+MneCJ48JJFVChQfuwKMp2yU9pmq1VKUR6ret0Gp0SjVYRRF+Xj7+OiUSk/GIzu1miHZWx+g8Y1RUktPmqIitRTXVNzzCtuFPKcH0zRBG+Y9/CnhBa20v5oHfsEUMgXMPEfO5ZcJx0FIPiVywgjb6MIuV+oZ4v2kk6/znIxDKrguM22y+bW8wUGqi7aL8fQJzwnCj8tIppdI9bYDSVJVCQInipW0HbtclcT7vCyLmXaSVrQSNMybaJJBh2PiXrXbgd6AbqecdDTO9EQEIeW0VPWQcdQ8ltPOEu+76q2IxUToJeWpfjQiHHH5AsADLj1bHgQxXsUoHfKYbg+CxCxC69eHcOvWheJ1l6b0nD7jG+bSA1dCZVxmw8ZJ/IYtxPtbJxlpQ/LGjSq00TmdNIZxrGel+y+rZJro+nUh3PrNIGwK6WrXNMV2xTeRWHSjScktLJfe1rc7spyvk3b6V4k48Sr3Am1Pv/QifhsI2uMvc863OiQQRNoedpPfHnSwcete+aDEE67cKzTgBlQgjpjgTDnJtGnX2qbmXJ6FOBLZ7wsr+JZzYnbjdbkCuEfU0HvlwqbtUgJ7zRXFNJsvSxlwz2WYta4xjri/fsulnnFVPyonpP0RL5oVNKkkfElG4csTDNAsgzC38G7gSKVgSZ7m/cEvKALmxKz//u7h6egHF7MrH4jJp/Zx4q32a8T71xnHVRCGlfFZNttd2FcUaay6e9PkhucyR0oPu1z1z/DB+8wixAFdMU1gnmB4xAw68pwHcWjlFrBnXxLjj63UGgvNGVGAJFzxFw+Womn7MAibVbu6leHRB5sc10fLtbrdr/JqV6Yr+ovwFtRHE7M4zG90qNB6YREoo51kFJabq3NeHVKdef/hsMFFSpt5m8XmJqDDAnR0c418mxmxrQzQuyPnspRwfAYkpthzr7gST1xNSf4WtBMM9DQT19uL+gb47gFLP3cT08F8I4dZxJl41Gsx9WHzLBOHzWjRS9NLCOUBCFQ+uGhB/V7ZzUwKESTmDriJ+UecdD/bFXFMLLsjgiAt4pp7ulpxb2tzE8I8xhyHODBK3SGg6QP12BiP3YMw2rDFtWUDXL+esnv3H9QxqfmbDnbMLjGUFpqqZbnWSg0lhWv9wU35qTHqP9zqUrL7kqKj8YjZzg01pb9+yQ8sXZpYxKGiFJTNsIwwpyR44gEOnV/+ennFdHD/2lQ3uS5y1qzIztXUNPE6odYJ0PqUiWJtgKGKMILY60dxeYynbb+sFKKqNn0Wz2rLtMbBQWPnYtmJa4WqFRob/9mmuycQVv7ifCNvXrlhzgDLDvAGA+8H5xjK948cDet+FaXfS+Lko/Wt+vScqarq6kZTbk4NaKqpObkEEpsac9L1rRNXJgPbrWyDdYje6tBQAztkbYC0wDe4UnNipmnZtInu/ujf6Kf7ve112Huf92Ev/7enB/+nP7pbrPiQJZbi0jCSpoN9UNPTkj7JMwpbWgopAbhtbOWkytAF3K+/qo0SASNW2G2bLfnshpB4a9dmz7/Hx//dc3OXNZ46YRyXUV2dYRsD97qKL79qazu+vSI1vPXT7375bWSGocBofD2eIRzJ0cMC0tenwQ0gfvuSdvd14f1uEooLPE3JJHL6uCd/n5n8d35UOKPn6nhr8kyrV3ad3nz2iTiNL414EnefL/JGLlWZtZWaqoEh4xSjvsGb/6m9raFlsLm4uHkQWlv7T/weZzjHHe7xZiUzpJ5WAWBLDNwRKxwRYnFoXGxcaKxN6DR8BNn2o9Nqmmutvra5TnIjXMBlmIFZ3yPYX3Mt9v5mmHuwYvvxPverL9eSvszXNjUXrkbqcGOVW2bEbDGKi3MLVTWzzWHF54Bu/2rA1qko6l9fFgVbBurfVBWFFlVW1ugxOwcs+8W//FcUZJieLl9WXA8eGL5crB7fhOMyxl8bjQWGjB1bW/ok6Ucqensr7F8H7utsmdqoHmz99rvyeE/Pz7u64mvVXLjyY8v8j5XhZeH3aPX75dpiO5eN/OzwcG7zkflt/sd5e7YcqbOowfRg22R5585at2vXX87W1Y0gQ079497eYT1EkyoEqMYABmHd8QvKGrRG6bJYTDCCZYGEWcm5G1jXM2i54Y9WtiBuklP57YtBZMAWlu2fYzDM7Q+5FmxKS3Oz5jwK6IactbWPowuQgNyHluKlaw9wnbOmtuajo/VSw9FrBSRwMcuUV2ZwFhh6s7hsqriWCsgA2s3nFcri4I7O+asxwxZbtLL03E9bhcR6Yz9mIbF0U96K0xGA7bx9y+l2//73j+H2i0EGd27uAVNI/WhCYuWqIDaYxads0lcVFV+dOlHmBx/qO7c6/uZX0tReUtJQv64y3adAvX6xDezAX/8Wm8Cgh/95O9OxsNCYnsXWQ+7pCz8/NMZ57ZAIGEdTw+ap8V+I3NUVe375wiv+lccqj172X7Yw5gJAUQGYPQ6QyxRfgeC+Qc5WnAMCAHFv6TJtet3pn/83b4YCAIBv35ofpTRyt5PjZEwT8KYAEQK8nFgBcE/yUwn2oqHSBKoEG7KZQLMpjo5uha/PI2yuBWOCTSDZajpqQ68+Za18jgGgYMT8nBhjKcFrKCYF6yKSZRLF5tR5YKhUzzNWM52mBvuPMiL7xPx4UaRgFiJZAVFscZ2HUIhUPcEaH5WWDvvmvdPfl5KaCvO8o1+fFCBb6hvuLz8lMROwfjPN8iar90RCCiRCJr3ugqHf6LqgUYYs5hzvu9tMIOUr/xpvRsNVvdZ/p+mB8n7V2Spo0T+aRhPpNhsNFOqxoE2u0suqTipgx58IJA0AAAA=) format('woff');
src: url(data:font/woff2;charset=utf-8;base64,d09GMgABAAAAAAtwAA0AAAAAFzwAAAsYAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP0ZGVE0cGhgGYACEHhEICp8QmBQLQAABNgIkA3wEIAWDGweCQBueEhEVqmUn+JmQuYW8aYRI8pyRytrpySK/jJBk9v+nbf19MKDMoI0Jwi77VwXMwsTIUSbEKvy5Veevw/8NG9XGRiobEQNstPsjEh74P+bfFxu1zQc2m+Aa+1eIVHCCRR1PoA82/wK/+v91bBjrxhAh4ktd/2kus/ntTjFbOEASKufInVMvm8JMpgyzruVsD5AssAcF1PIhpKsOUGhW6IGMOOvPDZlh7zJjzjrI26wbrOpZbwBAVfDQuPpH/fDP0TubwXIgpEa+JEjFsKsLM0KLQAQE14LrALAQORH0B0lBAopZoRo0lge+QsIkt7t+gH1ODaQKTBViUo7GA2pIRP8bMf8CyVQwgRDVMu155fOe5wufb3n+5YugFxkvrC9jXua83PHy4MuLo6+NJo1WjC4aXf5K8sr3K+Q2AIDnqTcbt7CFHniq7lfKm8oR5WblLABlM+L3CD7iA8VLxT0AxURRLf9RPiq/DiA/kPvLpQUwrWTSrYAbx6aq0GK5CHxuIQmcHz2lvxChFCIiUFFHKRJF8CoU/zMIYJApEPQffEglRnqtOgL79YqvGCYRhokEXIhkfaAnREw0kRC6CCNxkuxFQzw93OKS1xQKDBMlauQ1UrlcGhaKYZjEUyJRSqViQtRxRbSQ1FPtK/eKlmJsMY5hKgXG5TMXt6G2rDgyAMcjgoMR7poV+WDcbVxqvTsDvAORS+QQM2wZw3diBoGL38dToxuyzsk4nYkHkCVu770sO2Fz5JxyEU0u4z1XZVFkceJWXocPaTOA+92Io/YOLXvTRjscz0HAUTw/CLcyTucgZGFdrosTzUJc0IhwmBFxIUHvjr52qVjv4YUrN7ri+rW113ngEvY7yq4QtoprrqSDyOKsnsOs2IGfciFL4gFn+VXcWnqZj9/HvBpXLrdaRu++oO3foumwpo7cjd2ALMq997KOEraU4Tsx64GTD9xOPzSHXr48kFq2LIhZdmycVzhuXaHidowEmWhkxcpgdsUaZFkbvGorAleulNEr7ZFQBGFbruK2D8tMtL58WSC1fDVwqwLbbK4CKQbOkQsT5J+h+bzToxWcZVyF5zrfbEuz34vbiCwRQ3czj+DW5GO3deuAUwzeyTjcaHMA929BJ7K43rSVXXFcNw8cr6tbuxVZYoecmSM4MCPAAk0h62CVTLUrrnbVpsJvmDE8haaEtPppMQK24RCfXnfKVQLUAy/Ht7py69c7YsiTzrGEyTjsSIGLTv2Ugi+m0KUCTszXkmLkqMCLzykJcKEUeQkJgReLCMA5BNkNdBZQqM90YRZnfcA7YMjsRKx5A0gVV+Zqy/DDG60nkeUUYTsB3HEpdJomq5Px4nk8hLqL46/V3PI1iOqb5TsvEXNoCvA+gyOGRQRLE722LHtdTSzEuazpx3jdQKGtlOucGKNpB9r47WaOh1XHgVstq+BNHM9IQb3tlJAnne14goWMhJH+i7BmugtHUyiGXWmT0cu5OSANzUJCYD5TUGfV5qgybt8d2ptjQRNvkoyxHvBgBjy0U3Vsk5EmGwnAV20Zw46YwcwQ38CaOeKMHWq9mutb0IwI6BjVUp2ddFcP295j7qa66k0xbB3FkDRVy9Kd5touxjilliqq6Dl3au+49vURZaWX8eZYoN7q2qvjAjkH0R2y0Mz6TsrXxBkxwBed54FzzKFMHW2NTGejqZ5mWIahKJqaw6ywBkYxrJQKwkHWp1k6ahkno5fbgtmVlqA53LJAahePS607kGVlMNvvIrxs24EhPeV1lGUFElruEYTtDoBmRUWcO5BS19UMu9dsoRl6xRrnmhVw6lTC5s0/hEUnZq/a2TK1QF+YW1vucrEq1BfkaufOVdmJ0WE/JB0J6V1xUVF2k+n2FRWFXz9ge+bOjc2j6LwEOp0gidFOMOSxVcX2PPi6sclkcH9dXzWmSjVliqrMqK9neChRNQJrYn0xTR8w6D7N0KD56GfoFm40fbWQG1u26hMdqShSGHp7DWUmSb5xxSgUoyJ1n6zKNcYt/MoekjWjWD+tZ6nqYfV6Vl8wTV8c3UwUpE/k53d++613ZKQ5M//jmRTFKj9/5scGAInwr840R072vnlz53n2nIHKnvadXdtKzhD9ptX+JppBtmop95BUD6EKjbGptfPGv2rvjvpGHen9psduz22euz183owM+OfOaudtfcxiC5nCUCGOB0hodycVSuIn5TY05KovAu4+sjS4NGgW9fXXYYYJJwQkgs2Tll9cE6ib0LZa/LtO97t4ddsEHXZMLZVSXp6idgfuZSUPHjY1Da4vSQxv/PjpV9+qUvQ5BsO90RRuz6NkN0dr18R2G6C7f7fZfYqztKOYOTcE6ajlKbX2+u+T439nX3AH1FQVOyF+8gSP9Cq18eB1toItjLiuzUbxrJtjZUZFqbysZ6JhnEFd48l+0dxU09Brys839Zpqav6CjRAjgxyyyC9jhMRXGQCjvDhQmxpuD6dhgrQabZCmjysVs4fui/zjK0yVm7PSVOVwDxjCYJoEYg3r6MxMlYjyTFAMBSXrB33EwwergBt51BdGhYfRYeGGZ0SRAUoa2AWp2bKKqcbQ/EOGstEKsLxkK1SVqomTVF1RBcsSUKLSGmfHPHjwi4GpMre2FhczfAFj+CVwjoGUzo358yjz8uWVzx9XhNqhqJTRewZDjj6l2lYFHyd5T0lnZ4n614H7FFkUZgjrbXzytFgnFn/a1qarDKPCpR+aZ34oDS8K72ArdrzMNX0ZczW/HBhOrd6TyQ1/zHz2mj3OZJgXJ/SNXa6NG9mxadMvB6uqBODKrr7W2XlcDVFJRgPdjwYGhE51s4pqFIYp02xeCyJI0uChfBZZNaQuu9fc9W+uaJY22C59cruX1jNmkuyeptdP7/YRZsaCtemeaTcDyn17ZeW1qByao9275jJzl+4YHPaKyooP+lkdj1rKxVlUdJ5FFukzcvTe5vNQ8FrLIT2wg+Su9wXBHdX+1ZDSpyF/UXLmx94E9Lru5wr5wlWPTdg1F4G+i3Nn7e7ff38Nzt1+MY3bK1eBcUn9Iia2dJE/GUAyTa3gYUkJW03vYqnvva9uXWv7zbugvrmgoAYPq0j5MWA/3poasufX3zQxBH3lnzdTbbNzDclpZDVkvh758opB67FhwgnhTwydEWYPVbWpTXN4/uwRn9KB0v5hn3mzo49A+rLgx5/2qPb4LVny44+wvz+Smtq/bWtfH6Mi/OKgfvqxX9VvMo2oRiAn4J4LDeuGS0pUyv+kIz4Swy7FJWNuxXfjpooPdW3a/RvtQpHwjPaJdgVn+vw/rrjo/8rvSa6/ewdbfbJ/8yQw2NwGgrtZOCqMnqQnanm2tqWDAGsBIGWJKym6KTP0RAMwI+FgRhNHbTSJtNS/CdZpJCqQYkMOJW0qh9JiktoPV/fkRGhP1FClRpLdCCiqvQ0BwmoRD74QCVgUCVkeYax+uAhhphDHbFklLszd3/EoS8g7ASMBCyEhiyeMlQ4XCdImxMY+6Awu2TqY4S3duiRIlAhmbHd3JSReou4oNJuWqm03GfxIeUvA2xpaC6a/l8mmeN0E71BJECsNXL7sYUJsWhCy2KuYOgtzTr4TIG/XlhdY4x1mUSnTaYIqtrCiWHDPZKW9+aKyzgl/7/MoAAQUd6oEKIIgCEEYMCRCYuSBPJFkA/DIpMgLeSMf5Iv8kD8KQIFIhoJQMApBoSgMhSM5UnjIae98IFEfwn4XmrpvPULP5216LPpmNzJ0VXR2N7PD5G3zhL4UvaObbOiS7kvP5y2Ejm6OiiZiCjYBAAAA);
}
:root {
@@ -94,6 +94,11 @@ button {
cursor: pointer;
}
.iconlabel{
font-size:12px;
margin-top:2px;
}
.labels {
margin: 0;
padding: 8px 0 2px 0;
@@ -794,7 +799,7 @@ input[type=range]::-moz-range-thumb {
/* buttons */
.btn {
padding: 8px;
/*margin: 10px 4px;*/
margin: 10px 4px;
width: 230px;
font-size: 19px;
color: var(--c-d);
@@ -1476,7 +1481,7 @@ dialog {
.expanded {
display: inline-block !important;
}
.hide, .expanded .segin.hide, .expanded .presin.hide, .expanded .sbs.hide, .expanded .frz, .expanded .g-icon {
.hide, .expanded .segin.hide, .expanded .presin.hide, .expanded .sbs.hide, .expanded .g-icon {
display: none !important;
}
+24 -7
View File
@@ -10,7 +10,7 @@
<title>WLED</title>
<link rel="stylesheet" href="index.css">
</head>
<body onload="onLoad()">
<body>
<div id="cv" class="overlay">Loading WLED UI...</div>
<noscript><div class="overlay" style="opacity:1;">Sorry, WLED UI needs JavaScript!</div></noscript>
@@ -126,12 +126,28 @@
<input id="hexc" title="Hex RGB" type="text" class="noslide" onkeydown="hexEnter()" autocomplete="off" maxlength="8" />
<button id="hexcnf" class="btn btn-xs" onclick="fromHex();"><i class="icons btn-icon">&#xe390;</i></button>
</div>
<div style="padding: 8px 0;" id="btns">
<button class="btn btn-xs" title="File editor" type="button" id="edit" onclick="window.location.href=getURL('/edit')"><i class="icons btn-icon">&#xe2c6;</i></button>
<button class="btn btn-xs" title="Pixel Magic Tool" type="button" id="pxmb" onclick="window.location.href=getURL('/pxmagic.htm')"><i class="icons btn-icon">&#xe410;</i></button>
<button class="btn btn-xs" title="Add custom palette" type="button" id="adPal" onclick="window.location.href=getURL('/cpal.htm')"><i class="icons btn-icon">&#xe18a;</i></button>
<button class="btn btn-xs" title="Remove last custom palette" type="button" id="rmPal" onclick="palettesData=null;localStorage.removeItem('wledPalx');requestJson({rmcpal:true});setTimeout(loadPalettes,250,loadPalettesData);"><i class="icons btn-icon">&#xe037;</i></button>
<div style="padding: 12px 0; display:flex; justify-content:center; gap:16px;" id="btns">
<div>
<button class="btn btn-xs" title="File editor" type="button" onclick="window.location.href=getURL('/edit')">
<i class="icons btn-icon">&#xe901;</i>
</button>
<div class="iconlabel">Files</div>
</div>
<div>
<button class="btn btn-xs" title="PixelForge" type="button" onclick="window.location.href=getURL('/pixelforge.htm')">
<i class="icons btn-icon">&#xe900;</i>
</button>
<div class="iconlabel">PixelForge</div>
</div>
<div>
<button class="btn btn-xs" title="Custom palettes" type="button" onclick="window.location.href=getURL('/cpal.htm')">
<i class="icons btn-icon">&#xe2b3;</i>
</button>
<div class="iconlabel">Palettes</div>
</div>
</div>
<p class="labels hd" id="pall"><i class="icons sel-icon" onclick="tglHex()">&#xe2b3;</i> Color palette</p>
<div id="palw" class="il">
<div class="staytop fnd">
@@ -364,8 +380,9 @@
<!--
If you want to load iro.js and rangetouch.js as consecutive requests, you can do it like it was done in 0.14.0:
https://github.com/wled/WLED/blob/v0.14.0/wled00/data/index.htm
A more compact approach is implemented to load iro.js at the beginning of index.js.
-->
<script src="iro.js"></script>
<!-- <script src="iro.js"></script> NOTE: iro.js is loaded at the beginning of index.js -->
<script src="rangetouch.js"></script>
<script src="index.js"></script>
</body>

Some files were not shown because too many files have changed in this diff Show More