Compare commits

..

172 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
28d6e347ec Use 1024*1024 instead of magic number for bytes to MB conversion
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-11-28 18:10:05 +00:00
copilot-swe-agent[bot]
1b1f5811de Convert PSRAM from bytes to MB in usage reporting JavaScript
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-11-28 18:07:06 +00:00
copilot-swe-agent[bot]
ff184c1f41 Initial plan 2025-11-28 17:58:10 +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 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
Frank
d1c4de2499 Merge pull request #5107 from wled/s3_wroom2_32MB
update for esp32-S3 builds
* support for 32MB Flash
* added board manifest for adafruit matrixportal
* some cleanup, removed obsolete build flags
* new example how to "compile for speed" in platformio_orride.sample.ini
2025-11-25 16:23:55 +01:00
Damian Schneider
1e081a7f0d PS 1D Firwork bugfixes and improvements 2025-11-23 19:15:17 +01:00
Frank
730205ded5 AR: SR_DMTYPE=254 => UDP sound receive only (experimental)
additional dmtype = 254 "driver" that keeps AR enabled in "sound sync only" mode.
2025-11-23 00:24:49 +01:00
Frank
90ca6ccf8b AR: handle stupid build flag SR_DMTYPE=-1
I don't know how the bad example "-D SR_DMTYPE=-1" made it into platformio_override.sample.ini  🫣
mic type -1 = 255 was never supported by AR, and lead to undefined behavior due to a missing "case" in setup().

Fixed. Its still a stupid build_flags option, but at least now its handled properly.
2025-11-23 00:02:56 +01:00
Frank
d7fd49cc4c fix wrong -D SR_DMTYPE=-1 in platformio_override.sample.ini
SR_DMTYPE=-1 will lead to undefined behavior in AR, because for S3 there is no "default" case in the usermod setup(). It should be sufficient to set pins to "-1" if you want to avoid "pin stealing".
2025-11-22 23:38:49 +01:00
Frank
eb03520aa9 Update platformio_override.sample.ini
esp32S3_PSRAM_HUB75:
* use 16MB partinion.csv (board has 16MB flash, lets use that)
* example how to switch from "compile for small size" to "compile for speed"

adafruit_matrixportal_esp32s3:
* small reordering of lines
* commented out partition for adafruit bootloader, reverted to standard 8MB partitions
2025-11-22 23:07:59 +01:00
Frank
d8e2ceecf7 buildenv updates for adafruit MatrixPortal S3
* board.json added to WLED/boards
* use partitions file that supports adafruit UF2 bootloader
2025-11-22 21:24:23 +01:00
Frank
7dfed581b7 add esp32S3_wroom2 to default build
this board does not run with esp32s3dev_16MB_opi, because it needs "opi_opi" (not qio_opi) memory mode.
2025-11-22 20:42:43 +01:00
Frank
49a1ae54cf update for S3 buildenvs, and support for 32MB Flash
* removed obsolete "-D CONFIG_LITTLEFS_FOR_IDF_3_2" => this was only for the old "lorol/LITTLEFS" whic is not used any more in WLED
* commented out "-D ARDUINO_USB_MODE=1", because users have reported that it leads to boot "hanging" when no USB-CDC is connected
* Added buildenv and 32MB partition for esp32s3-WROOM-2 with 32MB flash
* disabled "-mfix-esp32-psram-cache-issue" warning for -S2 and -S3 (only necessary for classic esp32 "rev.1", but harmful on S3 or S2)
2025-11-22 20:35:53 +01:00
Frank
5b3cc753e2 partition files for use with ADAFRUIT boards
these partition files preserve the special "UF2" bootloader that is necessary for adafruit -S2 and -S3 boards.
2025-11-22 18:06:02 +01:00
Will Tatam
4615eb8258 Merge pull request #5093 from netmindz/deviceId
Add Device ID to JSON Info
2025-11-22 12:17:09 +00:00
Will Tatam
9b787e13d1 swap to using ESP.getFlashChipId for the 8266 2025-11-21 08:22:22 +00:00
Will Tatam
3dbcd79b3c Add efuse based data to salt 2025-11-21 08:17:38 +00:00
Will Tatam
a1aac452de use correct value for deviceString for 8266 and add comments 2025-11-20 00:24:24 +00:00
Will Tatam
b90fbe6b1a fix whitespace 2025-11-19 23:53:21 +00:00
Will Tatam
1860258deb deviceString for esp32 2025-11-19 23:48:30 +00:00
Will Tatam
a2935b87c2 deviceString for 8266 2025-11-19 23:45:55 +00:00
Will Tatam
c1ce1d8aba salt using additional hardware details 2025-11-19 23:11:31 +00:00
Frank
54b7dfe04b Fix debug message for servicing wait
forgot to adjust the debug condition in my previous commit.

NB: the condition only shows a debug message when the max wait time was exceeded, which can only happen when line 1692 has waited for the maximum allowed time. ->Is this intended?
2025-11-18 23:05:03 +01:00
Frank
4a33809d66 make waitForIt() timing logic robust against millis() rollover
the timing logic did not work in case that millis()+100 + frametime rolls over; in this case millis() > maxWait, and waiting would be skipped which might lead to crashes.
-> logic slightly adjusted to be robust against rollover.
2025-11-18 22:56:30 +01:00
Damian Schneider
336e074b4a fix for 0byte size files, also made reading ledmaps more efficient
when a ledmap is read from a file, it first parses the keys, putting the in front is more efficient as it will find them in the first 256 byte chunk.
2025-11-18 20:40:04 +01:00
Damian Schneider
aaad450175 show minimum of 0.1KB for small files in file editor 2025-11-18 07:26:17 +01:00
Will Tatam
85b3c5d91b refactor to use a common sha1 function 2025-11-18 05:53:12 +00:00
Will Tatam
4db86ebf7f Add salf and checksum 2025-11-18 05:35:49 +00:00
Damian Schneider
65c43b5224 add ctrl+s support to file editor, also add toast instead of alert 2025-11-17 20:56:49 +01:00
Will Tatam
c649ec1d8c Update wled00/json.cpp
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-11-17 17:40:09 +00:00
Frank
271e9ac7b7 image loader: allow graceful takeover after error
Allow decoder "takeover" by another segment
a) when last segment has decoding error (unsupported file, etc.)
b) when last segment became inactive
2025-11-17 14:45:07 +01:00
Damian Schneider
8348089b50 speed improvements to Aurora FX (#4926)
* improvements to Aurora FX

- converted to integer math, increasing speed on all ESPs, also shrinks code size
- caching values to avoid repeated calculations
- CRGBW instead or CRGB, adds white channel support when not using palette
- fix for new brightness/gamma handling

* overflow & unsigned fix
2025-11-16 21:20:14 +01:00
Damian Schneider
4f968861d6 fix for low heap situations on ESP8266 2025-11-16 12:59:35 +01:00
Will Tatam
66ffd65476 Add deviceId to JSON info respose, to be used for the post-upgrade notfication system 2025-11-15 20:17:23 +00:00
Damian Schneider
194829336f Fix OTA update for C3 from 0.15 (#5072)
* change C3 to DIO, add explicit QIO env for C3, add markOTAvalid() to support OTA from 0.15
2025-11-15 07:41:11 +01:00
Damian Schneider
f1d708ca43 Merge pull request #5073 from DedeHai/ledmap_bugfixes
Bugfix in ledmap generation
2025-11-15 07:39:39 +01:00
Damian Schneider
4f93661865 Improved 1D support for GIF images, bugfixes, blur option by @DedeHai & @softhack007
- add better support for 1D gifs: use the full gif, row by row, scale if needed
- add blur slider to image FX
- improved safety checks to avoid crashes
- add "fast path" if image size matches virtual segment size
2025-11-14 18:22:31 +01:00
Frank
cd2dc437a3 replace magic number by constant
32 => WLED_MAX_SEGNAME_LEN
2025-11-14 11:40:26 +01:00
Frank
f95dae1b1b ensure that lastFilename is always terminated properly 2025-11-14 01:40:46 +01:00
Frank
6ae4b1fc38 comment to prevent future "false improvement" attempts 2025-11-14 01:26:52 +01:00
Frank
fc776eeb16 add comment to explain coordinate packing logic 2025-11-14 01:08:48 +01:00
Damian Schneider
79376bbc58 improved lastCoordinate calculation 2025-11-13 18:26:00 +01:00
Damian Schneider
3b14c31e00 fix noScale callback, allow for more blur, removed some whitespaces 2025-11-11 21:09:48 +01:00
Damian Schneider
a666f07340 fix off-by-one bug, remove unnecessary 16384 restriction 2025-11-11 20:00:22 +01:00
Frank
bd933ff230 fix for "missing esp32 build flag"
In my previous commit I've overlooked that build_flags from esp32_idf_V4 are inherited by esp32S2, esp32s3 and esp32c3 --> clashed with USB-CTC settings of these boards.

So the correct way to propagate esp32-only flags is to add them in the "lower level" build envs individually.
2025-11-10 18:03:11 +01:00
Frank
7addae9c24 restore missing build flag for esp32
this flag got lost between 0.15 and 0.16.

Even when NO classic esp32 has USB-CDC., it seems that omitting the flag can cause strange behavior in the arduino-esp32 framework.
2025-11-10 16:52:24 +01:00
Frank
a73a2aaa33 restore missing platform_packages references
- restores compatibility with platformio_override.sample.ini
- best practice is to always define platform_packages, even when its empty = use default
2025-11-10 16:42:46 +01:00
Frank
a96e88043d remove commented code for no-PSRAM boards
*sigh* changing gifdecoder parameters seems to have _no_ effect on RAM needed
2025-11-09 20:24:57 +01:00
Frank
29d2f7fc1b debug print for decodeFrame error codes 2025-11-09 19:06:59 +01:00
Will Tatam
7aedf77d83 Merge pull request #4984 from wled/copilot/fix-d4f5fc55-f916-458a-9155-deb9bbff6662
Add ESP32 bootloader upgrade capability to OTA update page with JSON API support and ESP-IDF validation
2025-11-09 17:28:39 +00:00
Frank
1324d49098 revert smaller gif size limits for board without PSRAM
see discussion in PR#5040
2025-11-09 18:28:12 +01:00
Frank
79a52a60ff small optimization: fast 2D drawing without scaling
for 2D segments, setPixelColorXY() should be used because it is faster than  setPixelColor().
2025-11-09 18:14:50 +01:00
Frank
6581dd6ff9 add blur option 2025-11-09 17:33:04 +01:00
Frank
4659939547 error handling and robustness improvements
* catch some error that would lead to undefined behavior
* additional debug messages in case of errors
* robustness: handle OOM exception from  decoder.alloc() gracefully
2025-11-09 17:29:56 +01:00
Will Tatam
50d33c5bf4 Only supports ESP32 and ESP32-S2 2025-11-09 14:20:30 +00:00
Will Tatam
c7c379f962 match all esp32 types 2025-11-09 13:24:15 +00:00
Will Tatam
af8c851cc6 Privilege checks must run before bootloader init 2025-11-09 12:18:16 +00:00
Will Tatam
88466c7d1f Truncated bootloader images slip through verification and get flashed 2025-11-09 12:14:32 +00:00
Will Tatam
a36638ee6d Stop processing once an error is detected during bootloader upload 2025-11-09 12:04:56 +00:00
Will Tatam
ff93a48926 optimise fetching of bootloaderSHA256 2025-11-09 11:57:41 +00:00
Will Tatam
9474c29946 single definition of BOOTLOADER_OFFSET 2025-11-09 11:53:42 +00:00
Will Tatam
8097c7c86d refactor current bootloader reading out of the server ino ota 2025-11-09 11:41:51 +00:00
Will Tatam
abfe91d47b tidy up imports in wled_server.cpp 2025-11-09 11:31:56 +00:00
Will Tatam
a4109c7ea8 tidy up merge conflict on update.htm 2025-11-09 11:28:30 +00:00
Will Tatam
34445dbe0f working upgrade! (for esp32 classic) 2025-11-09 11:17:59 +00:00
Will Tatam
c5631b8fe3 remove null checks 2025-11-09 10:33:32 +00:00
Will Tatam
4cddd3face refactor bootloader update to pattern used by main ota update 2025-11-09 10:29:16 +00:00
Will Tatam
5250a0fe2c move verifyBootloaderImage to ota_update 2025-11-09 10:21:39 +00:00
Will Tatam
95611f19c0 Improve error handling 2025-11-09 10:14:17 +00:00
Will Tatam
b98ee3e7b6 remove duplicate updatebootloader handler 2025-11-09 09:53:05 +00:00
Will Tatam
90d4dd79de remove duplicate call for validation - isValidBootloader 2025-11-09 09:52:50 +00:00
Will Tatam
00904d8862 Merge branch 'copilot/fix-d4f5fc55-f916-458a-9155-deb9bbff6662' of https://github.com/wled/WLED into copilot/fix-d4f5fc55-f916-458a-9155-deb9bbff6662 2025-11-09 09:18:00 +00:00
copilot-swe-agent[bot]
2e7b6b79bf Initial plan 2025-11-09 09:17:06 +00:00
copilot-swe-agent[bot]
104d2ae7e8 Initial plan 2025-11-09 09:17:06 +00:00
copilot-swe-agent[bot]
f1242bfb7a Initial plan 2025-11-09 09:17:06 +00:00
copilot-swe-agent[bot]
aef5e9691c Initial plan 2025-11-09 09:17:06 +00:00
copilot-swe-agent[bot]
3d9012b43a Initial plan 2025-11-09 09:17:06 +00:00
copilot-swe-agent[bot]
3c5df5ae66 Initial plan 2025-11-09 09:17:06 +00:00
copilot-swe-agent[bot]
520f1f884b Enhance bootloader validation to match esp_image_verify() checks comprehensively
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-11-09 09:17:06 +00:00
copilot-swe-agent[bot]
8fc33fd7b1 Add ESP-IDF bootloader image validation before flash operations
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-11-09 09:17:06 +00:00
copilot-swe-agent[bot]
9e0f7ec4e9 Refactor bootloader upload to buffer entire file in RAM before flash operations
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-11-09 09:17:06 +00:00
Will Tatam
00ca694eea Fix: Move bootloader JavaScript to separate script block to avoid GetV() injection removal
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-11-09 09:16:59 +00:00
copilot-swe-agent[bot]
c91a39f55c Fix: Cast min() arguments to size_t for ESP32-C3 compatibility
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-11-09 09:15:36 +00:00
copilot-swe-agent[bot]
ffc7b66c20 Fix: Remove static keyword from getBootloaderSHA256Hex() to match declaration
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-11-09 09:15:36 +00:00
copilot-swe-agent[bot]
94bea4405a Improve bootloader flash implementation with proper erase and write operations
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-11-09 09:15:36 +00:00
copilot-swe-agent[bot]
2963c1b761 Add esp_flash.h include for ESP32 bootloader flash operations
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-11-09 09:15:36 +00:00
Will Tatam
013ecfb189 Add ESP32 bootloader upgrade functionality with JSON API support
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-11-09 09:15:23 +00:00
copilot-swe-agent[bot]
670f74d589 Initial plan 2025-11-09 09:10:36 +00:00
copilot-swe-agent[bot]
d475d21a79 Initial plan 2025-11-09 09:08:11 +00:00
copilot-swe-agent[bot]
91349234a0 Initial plan 2025-11-09 09:08:11 +00:00
copilot-swe-agent[bot]
601bb6f0ca Initial plan 2025-11-09 09:08:11 +00:00
Will Tatam
07e26d31f4 fix ESP32_DEBUG 2025-11-09 09:08:11 +00:00
copilot-swe-agent[bot]
151acb249e Initial plan 2025-11-09 09:08:11 +00:00
Will Tatam
25d5295d5d Merge pull request #5023 from DedeHai/matrix_save_fix
fix timing issue when changing 1D <-> 2D credits to @blazoncek
2025-11-09 08:13:22 +00:00
Will Tatam
474a995845 Merge pull request #5031 from wled/add-check-diff
Add segment checkmarks to `differs()` check
2025-11-09 08:03:10 +00:00
Damian Schneider
f0f12e77ad New file editor (#4956)
- no mendatory external JS dependency, works in offline mode
- optional external dependency is used for highlighting JSON, plain text edit is used if not available
- WLED styling (dark mode only)
- JSON files are displayed "prettyfied" and saved "minified"
- JSON color highlighting (if available)
- JSON verification during edit and on saving both in online and offline mode
- special treatment for ledmap files: displayed in aligned columns (2D) or as lines (1D), saved as minified json: no more white-space problems
- displays file size and total flash usage
2025-11-09 08:32:45 +01:00
Will Miles
46125773d9 Merge pull request #4998 from willmmiles/fix-4929-sq
Add OTA metadata validation v2
2025-11-08 17:41:42 -05:00
Will Tatam
ec61a35042 fix ESP32_DEBUG 2025-11-08 21:21:32 +00:00
copilot-swe-agent[bot]
1afd72cb83 Initial plan 2025-11-08 19:03:17 +00:00
Will Tatam
c623b82698 improve esp32_dev env 2025-11-08 19:03:17 +00:00
Will Tatam
acd415c522 fix release name for esp32 2025-11-08 19:03:17 +00:00
Will Tatam
5fb37130f8 Include esp32 debug build 2025-11-08 19:03:17 +00:00
Will Tatam
8e00e7175c Include audioreactive for hub75 examples - MOONHUB audio 2025-11-08 19:03:17 +00:00
Will Tatam
0f06535932 Include audioreactive for hub75 examples 2025-11-08 19:03:17 +00:00
Damian Schneider
46ff43889b check config backup as welcome page gate 2025-11-08 19:03:17 +00:00
Damian Schneider
7e1992fc5c adding function to check if a backup exists 2025-11-08 19:03:17 +00:00
Brandon502
0391488cef Game of Life Optimizations
Adjust mutation logic. Use 1D get/set. Reduce code size.
2025-11-08 19:03:17 +00:00
Brandon502
eb80fdf733 Game of Life Rework
RAM and speed optimizations. Better repeat detection. Mutation toggle. Blur option added.
2025-11-08 19:03:17 +00:00
Damian Schneider
4973fd5a39 Adding DDP over WS, moving duplicate WS-connection to common.js (#4997)
- Enabling DDP over WebSocket: this allows for UI or html tools to stream data to the LEDs much faster than through the JSON API.
- first byte of data array is used to determine protocol for future use
- Moved the duplicate function to establish a WS connection from the live-view htm files to common.js
- add better safety check for DDP: prevent OOB reads of buffer
2025-11-08 19:03:17 +00:00
Benjam Welker
1dd338c5e0 Fix blank area issue with Twinkle (#5005)
* Fix blank area issue with Twinkle
2025-11-08 19:03:17 +00:00
Damian Schneider
186c4a7724 fix low brightness gradient "jumpyness"
during testing at low brightness I noticed that gradients can be "jumping" in colors quite wildly, turning a smooth gradient into a flickering mess. This is due to the color hue preservation being inaccurate and a bit too aggressive. This can be seen for example using a gradient palette and "Running" FX.
Removing the hue preservation completely fixes it but leaves color artefacts for example visible in PS Fire at very low brightness: the bright part of the flames gets a pink hue. This change is a compromise to fix both problems to a "good enough" state
2025-11-08 19:03:17 +00:00
Damian Schneider
f0182eb1b2 safety check for bootloop action tracker: bring it back on track if out of bounds 2025-11-08 19:03:17 +00:00
wled-compile
2acf731baf Update platformio.ini
esp32dev_8M: add flash_mode
2025-11-08 19:03:17 +00:00
copilot-swe-agent[bot]
e2b8f91417 Reference Hardware Compilation section for common environments list
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-11-08 19:03:17 +00:00
copilot-swe-agent[bot]
5f33c69dd0 Fix copilot-instructions.md to require mandatory build validation
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-11-08 19:03:17 +00:00
copilot-swe-agent[bot]
76bb3f7d77 Initial plan 2025-11-08 19:03:17 +00:00
copilot-swe-agent[bot]
da1d53c3b0 Enhance bootloader validation to match esp_image_verify() checks comprehensively
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-11-08 18:56:31 +00:00
copilot-swe-agent[bot]
f4b98c43de Add ESP-IDF bootloader image validation before flash operations
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-11-08 18:31:20 +00:00
copilot-swe-agent[bot]
62c78fc5ac Refactor bootloader upload to buffer entire file in RAM before flash operations
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-11-08 18:00:24 +00:00
Will Tatam
9c4cf78a52 improve esp32_dev env 2025-11-08 15:17:21 +00:00
Damian Schneider
790be35ab8 make all globals static 2025-11-08 16:04:08 +01:00
Damian Schneider
0eef321f88 uising is2D() to check if segment is 2D, use vLength() on 1D setups 2025-11-08 12:54:25 +01:00
Damian Schneider
69dfe6c8a1 speed optimizations: skip setting multiple times, "fastpath" if no scaling needed 2025-11-08 12:01:40 +01:00
Will Tatam
80c97076ae fix release name for esp32 2025-11-07 18:29:03 +00:00
Will Tatam
c3f394489f Include esp32 debug build 2025-11-07 15:42:47 +00:00
Will Tatam
ce172df91a Include audioreactive for hub75 examples - MOONHUB audio 2025-11-07 15:18:01 +00:00
Will Tatam
91baa34071 Include audioreactive for hub75 examples 2025-11-07 15:18:01 +00:00
Damian Schneider
0e043b2a1b changed to vWidth/vHeight
- since we draw on a segment, we need to use virtual segment dimensions or scaling will be off when using any virtualisation like grouping/spacing/mirror etc.
2025-11-06 15:23:43 +01:00
Damian Schneider
01c84b0140 add better 1D support for gif images
Instead of showing a scaled, single line of the GIF: map the full gif to the strip
2025-11-06 14:55:26 +01:00
Blaž Kristan
1da2692c34 Add segment checkmarks to differs() check 2025-11-02 18:00:48 +01:00
Will Miles
d538736411 Always use package.json for WLED_VERSION
Ensures consistency between UI and metadata; fixes release bin names.
2025-10-26 17:58:23 -04:00
Will Miles
b268aea0ab set_metadata: Apply code fixes from @coderabbit 2025-10-25 13:43:10 -04:00
Will Miles
a04d70293d Fix set_metadata script 2025-10-25 09:58:01 -04:00
Will Miles
c66d67dd19 Fix metadata includes 2025-10-25 09:57:18 -04:00
Will Miles
0c22163fd9 Fix unaligned reads during metadata search 2025-10-25 09:57:03 -04:00
Damian Schneider
50c0f41508 fix timing issue when changing 1D <-> 2D credits to @blazoncek 2025-10-24 19:30:28 +02:00
Will Tatam
b60313e1f8 Merge pull request #5012 from DedeHai/improved_welcompage_check
Improvement to "Welcome Page" check
2025-10-22 20:40:06 +01:00
Will Tatam
1fff61b726 Merge pull request #4995 from Brandon502/GoLRework
Game of Life Rework
2025-10-21 20:43:28 +01:00
Damian Schneider
c4850aed08 Adding DDP over WS, moving duplicate WS-connection to common.js (#4997)
- Enabling DDP over WebSocket: this allows for UI or html tools to stream data to the LEDs much faster than through the JSON API.
- first byte of data array is used to determine protocol for future use
- Moved the duplicate function to establish a WS connection from the live-view htm files to common.js
- add better safety check for DDP: prevent OOB reads of buffer
2025-10-21 19:41:57 +02:00
Damian Schneider
43eb2d6f47 check config backup as welcome page gate 2025-10-19 07:10:05 +02:00
Damian Schneider
0ec4488dd1 adding function to check if a backup exists 2025-10-19 07:08:18 +02:00
Brandon502
cc0230f83f Game of Life Optimizations
Adjust mutation logic. Use 1D get/set. Reduce code size.
2025-10-14 22:58:22 -04:00
Brandon502
65b91762fd Game of Life Rework
RAM and speed optimizations. Better repeat detection. Mutation toggle. Blur option added.
2025-10-08 22:48:53 -04:00
Will Miles
5ca10f35d1 Process metadata only in metadata.cpp
Improves cache utilization as fewer things are passed via CFLAGS to
all files.  In the event that no metadata is available, let the cpp
file handle warning about default usage.
2025-10-06 21:52:16 -04:00
copilot-swe-agent[bot]
a073bf32e4 Implement OTA release compatibility checking system
Implement a comprehensive solution for validating a firmware before an
OTA updated is committed.  WLED metadata such as version and release
is moved to a data structure located at near the start of the firmware
binary, where it can be identified and validated.

Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-10-06 21:50:23 -04:00
copilot-swe-agent[bot]
d79b02379e Fix: Move bootloader JavaScript to separate script block to avoid GetV() injection removal
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-10-05 15:39:43 +00:00
copilot-swe-agent[bot]
f5f3fc338f Fix: Cast min() arguments to size_t for ESP32-C3 compatibility
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-10-05 15:12:46 +00:00
copilot-swe-agent[bot]
042ed39464 Fix: Remove static keyword from getBootloaderSHA256Hex() to match declaration
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-10-05 14:57:24 +00:00
copilot-swe-agent[bot]
c3e18905c1 Improve bootloader flash implementation with proper erase and write operations
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-10-05 14:24:29 +00:00
copilot-swe-agent[bot]
a18a661c73 Add esp_flash.h include for ESP32 bootloader flash operations
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-10-05 14:20:14 +00:00
copilot-swe-agent[bot]
93908e758f Add ESP32 bootloader upgrade functionality with JSON API support
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-10-05 14:17:51 +00:00
copilot-swe-agent[bot]
30fbf55b9a Initial plan 2025-10-05 14:09:10 +00:00
47 changed files with 3351 additions and 1289 deletions

View File

@@ -0,0 +1,66 @@
{
"build": {
"arduino":{
"ldscript": "esp32s3_out.ld",
"partitions": "partitions-8MB-tinyuf2.csv"
},
"core": "esp32",
"extra_flags": [
"-DARDUINO_ADAFRUIT_MATRIXPORTAL_ESP32S3",
"-DARDUINO_USB_CDC_ON_BOOT=1",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=1",
"-DBOARD_HAS_PSRAM"
],
"f_cpu": "240000000L",
"f_flash": "80000000L",
"flash_mode": "qio",
"hwids": [
[
"0x239A",
"0x8125"
],
[
"0x239A",
"0x0125"
],
[
"0x239A",
"0x8126"
]
],
"mcu": "esp32s3",
"variant": "adafruit_matrixportal_esp32s3"
},
"connectivity": [
"bluetooth",
"wifi"
],
"debug": {
"openocd_target": "esp32s3.cfg"
},
"frameworks": [
"arduino",
"espidf"
],
"name": "Adafruit MatrixPortal ESP32-S3",
"upload": {
"arduino": {
"flash_extra_images": [
[
"0x410000",
"variants/adafruit_matrixportal_esp32s3/tinyuf2.bin"
]
]
},
"flash_size": "8MB",
"maximum_ram_size": 327680,
"maximum_size": 8388608,
"use_1200bps_touch": true,
"wait_for_upload_port": true,
"require_upload_port": true,
"speed": 460800
},
"url": "https://www.adafruit.com/product/5778",
"vendor": "Adafruit"
}

View File

@@ -2,6 +2,7 @@ Import('env')
import os
import shutil
import gzip
import json
OUTPUT_DIR = "build_output{}".format(os.path.sep)
#OUTPUT_DIR = os.path.join("build_output")
@@ -22,7 +23,8 @@ def create_release(source):
release_name_def = _get_cpp_define_value(env, "WLED_RELEASE_NAME")
if release_name_def:
release_name = release_name_def.replace("\\\"", "")
version = _get_cpp_define_value(env, "WLED_VERSION")
with open("package.json", "r") as package:
version = json.load(package)["version"]
release_file = os.path.join(OUTPUT_DIR, "release", f"WLED_{version}_{release_name}.bin")
release_gz_file = release_file + ".gz"
print(f"Copying {source} to {release_file}")

View File

@@ -1,5 +1,6 @@
Import('env')
import subprocess
import json
import re
def get_github_repo():
@@ -42,7 +43,7 @@ def get_github_repo():
# Check if it's a GitHub URL
if 'github.com' not in remote_url.lower():
return 'unknown'
return None
# Parse GitHub URL patterns:
# https://github.com/owner/repo.git
@@ -63,17 +64,53 @@ def get_github_repo():
if ssh_match:
return ssh_match.group(1)
return 'unknown'
return None
except FileNotFoundError:
# Git CLI is not installed or not in PATH
return 'unknown'
return None
except subprocess.CalledProcessError:
# Git command failed (e.g., not a git repo, no remote, etc.)
return 'unknown'
return None
except Exception:
# Any other unexpected error
return 'unknown'
return None
repo = get_github_repo()
env.Append(BUILD_FLAGS=[f'-DWLED_REPO=\\"{repo}\\"'])
# WLED version is managed by package.json; this is picked up in several places
# - It's integrated in to the UI code
# - Here, for wled_metadata.cpp
# - The output_bins script
# We always take it from package.json to ensure consistency
with open("package.json", "r") as package:
WLED_VERSION = json.load(package)["version"]
def has_def(cppdefs, name):
""" Returns true if a given name is set in a CPPDEFINES collection """
for f in cppdefs:
if isinstance(f, tuple):
f = f[0]
if f == name:
return True
return False
def add_wled_metadata_flags(env, node):
cdefs = env["CPPDEFINES"].copy()
if not has_def(cdefs, "WLED_REPO"):
repo = get_github_repo()
if repo:
cdefs.append(("WLED_REPO", f"\\\"{repo}\\\""))
cdefs.append(("WLED_VERSION", WLED_VERSION))
# This transforms the node in to a Builder; it cannot be modified again
return env.Object(
node,
CPPDEFINES=cdefs
)
env.AddBuildMiddleware(
add_wled_metadata_flags,
"*/wled_metadata.cpp"
)

View File

@@ -1,8 +0,0 @@
Import('env')
import json
PACKAGE_FILE = "package.json"
with open(PACKAGE_FILE, "r") as package:
version = json.load(package)["version"]
env.Append(BUILD_FLAGS=[f"-DWLED_VERSION={version}"])

View File

@@ -10,7 +10,27 @@
# ------------------------------------------------------------------------------
# CI/release binaries
default_envs = nodemcuv2, esp8266_2m, esp01_1m_full, nodemcuv2_160, esp8266_2m_160, esp01_1m_full_160, nodemcuv2_compat, esp8266_2m_compat, esp01_1m_full_compat, esp32dev, esp32_eth, lolin_s2_mini, esp32c3dev, esp32s3dev_16MB_opi, esp32s3dev_8MB_opi, esp32s3_4M_qspi, esp32_wrover, usermods
default_envs = nodemcuv2
esp8266_2m
esp01_1m_full
nodemcuv2_160
esp8266_2m_160
esp01_1m_full_160
nodemcuv2_compat
esp8266_2m_compat
esp01_1m_full_compat
esp32dev
esp32dev_debug
esp32_eth
esp32_wrover
lolin_s2_mini
esp32c3dev
esp32c3dev_qio
esp32S3_wroom2
esp32s3dev_16MB_opi
esp32s3dev_8MB_opi
esp32s3_4M_qspi
usermods
src_dir = ./wled00
data_dir = ./wled00/data
@@ -110,8 +130,7 @@ ldscript_4m1m = eagle.flash.4m1m.ld
[scripts_defaults]
extra_scripts =
pre:pio-scripts/set_version.py
pre:pio-scripts/set_repo.py
pre:pio-scripts/set_metadata.py
post:pio-scripts/output_bins.py
post:pio-scripts/strip-floats.py
pre:pio-scripts/user_config_copy.py
@@ -265,12 +284,14 @@ AR_lib_deps = ;; for pre-usermod-library platformio_override compatibility
[esp32_idf_V4]
;; build environment for ESP32 using ESP-IDF 4.4.x / arduino-esp32 v2.0.5
;; *** important: build flags from esp32_idf_V4 are inherited by _all_ esp32-based MCUs: esp32, esp32s2, esp32s3, esp32c3
;;
;; please note that you can NOT update existing ESP32 installs with a "V4" build. Also updating by OTA will not work properly.
;; You need to completely erase your device (esptool erase_flash) first, then install the "V4" build from VSCode+platformio.
;; 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
platform_packages =
build_unflags = ${common.build_unflags}
build_flags = -g
-Wshadow=compatible-local ;; emit warning in case a local variable "shadows" another local one
@@ -285,6 +306,7 @@ lib_deps =
[esp32s2]
;; generic definitions for all ESP32-S2 boards
platform = ${esp32_idf_V4.platform}
platform_packages = ${esp32_idf_V4.platform_packages}
build_unflags = ${common.build_unflags}
build_flags = -g
-DARDUINO_ARCH_ESP32
@@ -303,6 +325,7 @@ board_build.partitions = ${esp32.default_partitions} ;; default partioning for
[esp32c3]
;; generic definitions for all ESP32-C3 boards
platform = ${esp32_idf_V4.platform}
platform_packages = ${esp32_idf_V4.platform_packages}
build_unflags = ${common.build_unflags}
build_flags = -g
-DARDUINO_ARCH_ESP32
@@ -321,6 +344,7 @@ board_build.flash_mode = qio
[esp32s3]
;; generic definitions for all ESP32-S3 boards
platform = ${esp32_idf_V4.platform}
platform_packages = ${esp32_idf_V4.platform_packages}
build_unflags = ${common.build_unflags}
build_flags = -g
-DESP32
@@ -429,21 +453,31 @@ custom_usermods = audioreactive
[env:esp32dev]
board = esp32dev
platform = ${esp32_idf_V4.platform}
platform_packages = ${esp32_idf_V4.platform_packages}
build_unflags = ${common.build_unflags}
custom_usermods = audioreactive
build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=\"ESP32_V4\" #-D WLED_DISABLE_BROWNOUT_DET
build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=\"ESP32\" #-D WLED_DISABLE_BROWNOUT_DET
-DARDUINO_USB_CDC_ON_BOOT=0 ;; this flag is mandatory for "classic ESP32" when building with arduino-esp32 >=2.0.3
lib_deps = ${esp32_idf_V4.lib_deps}
monitor_filters = esp32_exception_decoder
board_build.partitions = ${esp32.default_partitions}
board_build.flash_mode = dio
[env:esp32dev_debug]
extends = env:esp32dev
upload_speed = 921600
build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags}
-D WLED_DEBUG
-D WLED_RELEASE_NAME=\"ESP32_DEBUG\"
[env:esp32dev_8M]
board = esp32dev
platform = ${esp32_idf_V4.platform}
platform_packages = ${esp32_idf_V4.platform_packages}
custom_usermods = audioreactive
build_unflags = ${common.build_unflags}
build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=\"ESP32_8M\" #-D WLED_DISABLE_BROWNOUT_DET
-DARDUINO_USB_CDC_ON_BOOT=0 ;; this flag is mandatory for "classic ESP32" when building with arduino-esp32 >=2.0.3
lib_deps = ${esp32_idf_V4.lib_deps}
monitor_filters = esp32_exception_decoder
board_build.partitions = ${esp32.large_partitions}
@@ -455,9 +489,11 @@ board_build.flash_mode = dio
[env:esp32dev_16M]
board = esp32dev
platform = ${esp32_idf_V4.platform}
platform_packages = ${esp32_idf_V4.platform_packages}
custom_usermods = audioreactive
build_unflags = ${common.build_unflags}
build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=\"ESP32_16M\" #-D WLED_DISABLE_BROWNOUT_DET
-DARDUINO_USB_CDC_ON_BOOT=0 ;; this flag is mandatory for "classic ESP32" when building with arduino-esp32 >=2.0.3
lib_deps = ${esp32_idf_V4.lib_deps}
monitor_filters = esp32_exception_decoder
board_build.partitions = ${esp32.extreme_partitions}
@@ -469,10 +505,12 @@ board_build.flash_mode = dio
[env:esp32_eth]
board = esp32-poe
platform = ${esp32_idf_V4.platform}
platform_packages = ${esp32_idf_V4.platform_packages}
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
-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}
@@ -487,6 +525,7 @@ board_build.partitions = ${esp32.extended_partitions}
custom_usermods = audioreactive
build_unflags = ${common.build_unflags}
build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=\"ESP32_WROVER\"
-DARDUINO_USB_CDC_ON_BOOT=0 ;; this flag is mandatory for "classic ESP32" when building with arduino-esp32 >=2.0.3
-DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue ;; Older ESP32 (rev.<3) need a PSRAM fix (increases static RAM used) https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-guides/external-ram.html
-D DATA_PINS=25
lib_deps = ${esp32_idf_V4.lib_deps}
@@ -494,6 +533,7 @@ lib_deps = ${esp32_idf_V4.lib_deps}
[env:esp32c3dev]
extends = esp32c3
platform = ${esp32c3.platform}
platform_packages = ${esp32c3.platform_packages}
framework = arduino
board = esp32-c3-devkitm-1
board_build.partitions = ${esp32.default_partitions}
@@ -505,19 +545,26 @@ build_flags = ${common.build_flags} ${esp32c3.build_flags} -D WLED_RELEASE_NAME=
upload_speed = 460800
build_unflags = ${common.build_unflags}
lib_deps = ${esp32c3.lib_deps}
board_build.flash_mode = dio ; safe default, required for OTA updates to 0.16 from older version which used dio (must match the bootloader!)
[env:esp32c3dev_qio]
extends = env:esp32c3dev
build_flags = ${common.build_flags} ${esp32c3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-C3-QIO\"
board_build.flash_mode = qio ; qio is faster and works on almost all boards (some boards may use dio to get 2 extra pins)
[env:esp32s3dev_16MB_opi]
;; ESP32-S3 development board, with 16MB FLASH and >= 8MB PSRAM (memory_type: qio_opi)
board = esp32-s3-devkitc-1 ;; generic dev board; the next line adds PSRAM support
board_build.arduino.memory_type = qio_opi ;; use with PSRAM: 8MB or 16MB
platform = ${esp32s3.platform}
platform_packages = ${esp32s3.platform_packages}
upload_speed = 921600
custom_usermods = audioreactive
build_unflags = ${common.build_unflags}
build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_16MB_opi\"
-D CONFIG_LITTLEFS_FOR_IDF_3_2 -D WLED_WATCHDOG_TIMEOUT=0
-D WLED_WATCHDOG_TIMEOUT=0
;-D ARDUINO_USB_CDC_ON_BOOT=0 ;; -D ARDUINO_USB_MODE=1 ;; for boards with serial-to-USB chip
-D ARDUINO_USB_CDC_ON_BOOT=1 -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB")
-D ARDUINO_USB_CDC_ON_BOOT=1 ;; -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB")
-DBOARD_HAS_PSRAM
lib_deps = ${esp32s3.lib_deps}
board_build.partitions = ${esp32.extreme_partitions}
@@ -532,13 +579,14 @@ monitor_filters = esp32_exception_decoder
board = esp32-s3-devkitc-1 ;; generic dev board; the next line adds PSRAM support
board_build.arduino.memory_type = qio_opi ;; use with PSRAM: 8MB or 16MB
platform = ${esp32s3.platform}
platform_packages = ${esp32s3.platform_packages}
upload_speed = 921600
custom_usermods = audioreactive
build_unflags = ${common.build_unflags}
build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_8MB_opi\"
-D CONFIG_LITTLEFS_FOR_IDF_3_2 -D WLED_WATCHDOG_TIMEOUT=0
-D WLED_WATCHDOG_TIMEOUT=0
;-D ARDUINO_USB_CDC_ON_BOOT=0 ;; -D ARDUINO_USB_MODE=1 ;; for boards with serial-to-USB chip
-D ARDUINO_USB_CDC_ON_BOOT=1 -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB")
-D ARDUINO_USB_CDC_ON_BOOT=1 ;; -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB")
-DBOARD_HAS_PSRAM
lib_deps = ${esp32s3.lib_deps}
board_build.partitions = ${esp32.large_partitions}
@@ -550,19 +598,20 @@ monitor_filters = esp32_exception_decoder
;; For ESP32-S3 WROOM-2, a.k.a. ESP32-S3 DevKitC-1 v1.1
;; with >= 16MB FLASH and >= 8MB PSRAM (memory_type: opi_opi)
platform = ${esp32s3.platform}
platform_packages = ${esp32s3.platform_packages}
board = esp32s3camlcd ;; this is the only standard board with "opi_opi"
board_build.arduino.memory_type = opi_opi
upload_speed = 921600
custom_usermods = audioreactive
build_unflags = ${common.build_unflags}
build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_WROOM-2\"
-D CONFIG_LITTLEFS_FOR_IDF_3_2 -D WLED_WATCHDOG_TIMEOUT=0
-D WLED_WATCHDOG_TIMEOUT=0
-D ARDUINO_USB_CDC_ON_BOOT=0 ;; -D ARDUINO_USB_MODE=1 ;; for boards with serial-to-USB chip
;; -D ARDUINO_USB_CDC_ON_BOOT=1 -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB")
;; -D ARDUINO_USB_CDC_ON_BOOT=1 ;; -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB")
-DBOARD_HAS_PSRAM
-D LEDPIN=38 -D DATA_PINS=38 ;; buildin WS2812b LED
-D BTNPIN=0 -D RLYPIN=16 -D IRPIN=17 -D AUDIOPIN=-1
-D WLED_DEBUG
;;-D WLED_DEBUG
-D SR_DMTYPE=1 -D I2S_SDPIN=13 -D I2S_CKPIN=14 -D I2S_WSPIN=15 -D MCLK_PIN=4 ;; I2S mic
lib_deps = ${esp32s3.lib_deps}
@@ -571,15 +620,33 @@ board_upload.flash_size = 16MB
board_upload.maximum_size = 16777216
monitor_filters = esp32_exception_decoder
[env:esp32S3_wroom2_32MB]
;; For ESP32-S3 WROOM-2 with 32MB Flash, and >= 8MB PSRAM (memory_type: opi_opi)
extends = env:esp32S3_wroom2
build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_WROOM-2_32MB\"
-D WLED_WATCHDOG_TIMEOUT=0
-D ARDUINO_USB_CDC_ON_BOOT=0 ;; -D ARDUINO_USB_MODE=1 ;; for boards with serial-to-USB chip
;; -D ARDUINO_USB_CDC_ON_BOOT=1 ;; -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB")
-DBOARD_HAS_PSRAM
-D LEDPIN=38 -D DATA_PINS=38 ;; buildin WS2812b LED
-D BTNPIN=0 -D RLYPIN=16 -D IRPIN=17 -D AUDIOPIN=-1
;;-D WLED_DEBUG
-D SR_DMTYPE=1 -D I2S_SDPIN=13 -D I2S_CKPIN=14 -D I2S_WSPIN=15 -D MCLK_PIN=4 ;; I2S mic
board_build.partitions = tools/WLED_ESP32_32MB.csv
board_upload.flash_size = 32MB
board_upload.maximum_size = 33554432
monitor_filters = esp32_exception_decoder
[env:esp32s3_4M_qspi]
;; ESP32-S3, with 4MB FLASH and <= 4MB PSRAM (memory_type: qio_qspi)
board = lolin_s3_mini ;; -S3 mini, 4MB flash 2MB PSRAM
platform = ${esp32s3.platform}
platform_packages = ${esp32s3.platform_packages}
upload_speed = 921600
custom_usermods = audioreactive
build_unflags = ${common.build_unflags}
build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_4M_qspi\"
-DARDUINO_USB_CDC_ON_BOOT=1 -DARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB")
-DARDUINO_USB_CDC_ON_BOOT=1 ;; -DARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB")
-DBOARD_HAS_PSRAM
-DLOLIN_WIFI_FIX ; seems to work much better with this
-D WLED_WATCHDOG_TIMEOUT=0
@@ -591,6 +658,7 @@ monitor_filters = esp32_exception_decoder
[env:lolin_s2_mini]
platform = ${esp32s2.platform}
platform_packages = ${esp32s2.platform_packages}
board = lolin_s2_mini
board_build.partitions = ${esp32.default_partitions}
board_build.flash_mode = qio
@@ -617,6 +685,7 @@ lib_deps = ${esp32s2.lib_deps}
[env:usermods]
board = esp32dev
platform = ${esp32_idf_V4.platform}
platform_packages = ${esp32_idf_V4.platform_packages}
build_unflags = ${common.build_unflags}
build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=\"ESP32_USERMODS\"
-DTOUCH_CS=9

View File

@@ -191,6 +191,22 @@ build_flags = ${common.build_flags} ${esp8266.build_flags}
; -D HW_PIN_MISOSPI=9
# ------------------------------------------------------------------------------
# Optional: build flags for speed, instead of optimising for size.
# Example of usage: see [env:esp32S3_PSRAM_HUB75]
# ------------------------------------------------------------------------------
[Speed_Flags]
build_unflags = -Os ;; to disable standard optimization for small size
build_flags =
-O2 ;; optimize for speed
-free -fipa-pta ;; very useful, too
;;-fsingle-precision-constant ;; makes all floating point literals "float" (default is "double")
;;-funsafe-math-optimizations ;; less dangerous than -ffast-math; still allows the compiler to exploit FMA and reciprocals (up to 10% faster on -S3)
# Important: we need to explicitly switch off some "-O2" optimizations
-fno-jump-tables -fno-tree-switch-conversion ;; needed - firmware may crash otherwise
-freorder-blocks -Wwrite-strings -fstrict-volatile-bitfields ;; needed - recommended by espressif
# ------------------------------------------------------------------------------
# PRE-CONFIGURED DEVELOPMENT BOARDS AND CONTROLLERS
@@ -541,12 +557,15 @@ build_flags = ${common.build_flags}
-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
@@ -555,19 +574,22 @@ build_flags = ${common.build_flags}
-D WLED_ENABLE_HUB75MATRIX -D NO_GFX
-D ESP32_FORUM_PINOUT ;; enable for SmartMatrix default pins
-D WLED_DEBUG_BUS
-D SR_DMTYPE=1 -D I2S_SDPIN=-1 -D I2S_CKPIN=-1 -D I2S_WSPIN=-1 -D MCLK_PIN=-1 ;; Disable to prevent pin clash
; -D WLED_DEBUG
[env:adafruit_matrixportal_esp32s3]
; ESP32-S3 processor, 8 MB flash, 2 MB of PSRAM, dedicated driver pins for HUB75
board = adafruit_matrixportal_esp32s3
;; 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_4M_qspi\"
-DARDUINO_USB_CDC_ON_BOOT=1 -DARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB")
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
@@ -575,25 +597,30 @@ build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=
-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.default_partitions}
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
;; 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\"
-DARDUINO_USB_CDC_ON_BOOT=1 -DARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB")
${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
@@ -601,11 +628,15 @@ build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=
-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.default_partitions}
;;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

View File

@@ -0,0 +1,7 @@
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 0xe000, 0x2000,
app0, app, ota_0, 0x10000, 0x300000,
app1, app, ota_1, 0x310000,0x300000,
spiffs, data, spiffs, 0x610000,0x19E0000,
coredump, data, coredump,,64K
1 # Name, Type, SubType, Offset, Size, Flags
2 nvs, data, nvs, 0x9000, 0x5000,
3 otadata, data, ota, 0xe000, 0x2000,
4 app0, app, ota_0, 0x10000, 0x300000,
5 app1, app, ota_1, 0x310000,0x300000,
6 spiffs, data, spiffs, 0x610000,0x19E0000,
7 coredump, data, coredump,,64K

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_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_settings.h", "wled00/html_other.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()}
`;
@@ -246,6 +254,21 @@ 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/edit.htm", "wled00/html_edit.h", 'edit');
writeChunks(
"wled00/data",
[
{
file: "edit.htm",
name: "PAGE_edit",
method: "gzip",
filter: "html-minify"
}
],
"wled00/html_edit.h"
);
writeChunks(
"wled00/data/cpal",
@@ -388,12 +411,6 @@ const char PAGE_dmxmap[] PROGMEM = R"=====()=====";
name: "PAGE_update",
method: "gzip",
filter: "html-minify",
mangle: (str) =>
str
.replace(
/function GetV().*\<\/script\>/gms,
"</script><script src=\"/settings/s.js?p=9\"></script>"
)
},
{
file: "welcome.htm",

View File

@@ -0,0 +1,10 @@
# ESP-IDF Partition Table
# Name, Type, SubType, Offset, Size, Flags
# bootloader.bin,, 0x1000, 32K
# partition table,, 0x8000, 4K
nvs, data, nvs, 0x9000, 20K,
otadata, data, ota, 0xe000, 8K,
ota_0, app, ota_0, 0x10000, 2048K,
ota_1, app, ota_1, 0x210000, 2048K,
uf2, app, factory,0x410000, 256K,
spiffs, data, spiffs, 0x450000, 11968K,
1 # ESP-IDF Partition Table
2 # Name, Type, SubType, Offset, Size, Flags
3 # bootloader.bin,, 0x1000, 32K
4 # partition table,, 0x8000, 4K
5 nvs, data, nvs, 0x9000, 20K,
6 otadata, data, ota, 0xe000, 8K,
7 ota_0, app, ota_0, 0x10000, 2048K,
8 ota_1, app, ota_1, 0x210000, 2048K,
9 uf2, app, factory,0x410000, 256K,
10 spiffs, data, spiffs, 0x450000, 11968K,

View File

@@ -0,0 +1,11 @@
# ESP-IDF Partition Table
# Name, Type, SubType, Offset, Size, Flags
# bootloader.bin,, 0x1000, 32K
# partition table, 0x8000, 4K
nvs, data, nvs, 0x9000, 20K,
otadata, data, ota, 0xe000, 8K,
ota_0, 0, ota_0, 0x10000, 1408K,
ota_1, 0, ota_1, 0x170000, 1408K,
uf2, app, factory,0x2d0000, 256K,
spiffs, data, spiffs, 0x310000, 960K,
1 # ESP-IDF Partition Table
2 # Name, Type, SubType, Offset, Size, Flags
3 # bootloader.bin,, 0x1000, 32K
4 # partition table, 0x8000, 4K
5 nvs, data, nvs, 0x9000, 20K,
6 otadata, data, ota, 0xe000, 8K,
7 ota_0, 0, ota_0, 0x10000, 1408K,
8 ota_1, 0, ota_1, 0x170000, 1408K,
9 uf2, app, factory,0x2d0000, 256K,
10 spiffs, data, spiffs, 0x310000, 960K,

View File

@@ -0,0 +1,10 @@
# ESP-IDF Partition Table
# Name, Type, SubType, Offset, Size, Flags
# bootloader.bin,, 0x1000, 32K
# partition table,, 0x8000, 4K
nvs, data, nvs, 0x9000, 20K,
otadata, data, ota, 0xe000, 8K,
ota_0, app, ota_0, 0x10000, 2048K,
ota_1, app, ota_1, 0x210000, 2048K,
uf2, app, factory,0x410000, 256K,
spiffs, data, spiffs, 0x450000, 3776K,
1 # ESP-IDF Partition Table
2 # Name, Type, SubType, Offset, Size, Flags
3 # bootloader.bin,, 0x1000, 32K
4 # partition table,, 0x8000, 4K
5 nvs, data, nvs, 0x9000, 20K,
6 otadata, data, ota, 0xe000, 8K,
7 ota_0, app, ota_0, 0x10000, 2048K,
8 ota_1, app, ota_1, 0x210000, 2048K,
9 uf2, app, factory,0x410000, 256K,
10 spiffs, data, spiffs, 0x450000, 3776K,

View File

@@ -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;
}

View File

@@ -1227,7 +1227,6 @@ class AudioReactive : public Usermod {
#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3)
// ADC over I2S is only possible on "classic" ESP32
case 0:
default:
DEBUGSR_PRINTLN(F("AR: Analog Microphone (left channel only)."));
audioSource = new I2SAdcSource(SAMPLE_RATE, BLOCK_SIZE);
delay(100);
@@ -1235,10 +1234,25 @@ class AudioReactive : public Usermod {
if (audioSource) audioSource->initialize(audioPin);
break;
#endif
case 254: // dummy "network receive only" mode
if (audioSource) delete audioSource; audioSource = nullptr;
disableSoundProcessing = true;
audioSyncEnabled = 2; // force udp sound receive mode
enabled = true;
break;
case 255: // 255 = -1 = no audio source
// falls through to default
default:
if (audioSource) delete audioSource; audioSource = nullptr;
disableSoundProcessing = true;
enabled = false;
break;
}
delay(250); // give microphone enough time to initialise
if (!audioSource) enabled = false; // audio failed to initialise
if (!audioSource && (dmType != 254)) enabled = false;// audio failed to initialise
#endif
if (enabled) onUpdateBegin(false); // create FFT task, and initialize network
@@ -1530,7 +1544,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;
}

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) {

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);
}

View File

@@ -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;
}

View File

@@ -15,14 +15,25 @@
#include "fcn_declare.h"
#if !(defined(WLED_DISABLE_PARTICLESYSTEM2D) && defined(WLED_DISABLE_PARTICLESYSTEM1D))
#include "FXparticleSystem.h"
#include "FXparticleSystem.h" // include particle system code only if at least one system is enabled
#ifdef WLED_DISABLE_PARTICLESYSTEM2D
#define WLED_PS_DONT_REPLACE_2D_FX
#endif
#ifdef WLED_DISABLE_PARTICLESYSTEM1D
#define WLED_PS_DONT_REPLACE_1D_FX
#endif
#ifdef ESP8266
#if !defined(WLED_DISABLE_PARTICLESYSTEM2D) && !defined(WLED_DISABLE_PARTICLESYSTEM1D)
#error ESP8266 does not support 1D and 2D particle systems simultaneously. Please disable one of them.
#error ESP8266 does not support 1D and 2D particle systems simultaneously. Please disable one of them.
#endif
#endif
#else
#define WLED_PS_DONT_REPLACE_FX
#define WLED_PS_DONT_REPLACE_1D_FX
#define WLED_PS_DONT_REPLACE_2D_FX
#endif
#ifdef WLED_PS_DONT_REPLACE_FX
#define WLED_PS_DONT_REPLACE_1D_FX
#define WLED_PS_DONT_REPLACE_2D_FX
#endif
//////////////
@@ -713,7 +724,7 @@ uint16_t dissolve(uint32_t color) {
if (SEGENV.aux0) { //dissolve to primary/palette
if (pixels[i] == SEGCOLOR(1)) {
pixels[i] = color == SEGCOLOR(0) ? SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0) : color;
break; //only spawn 1 new pixel per frame per 50 LEDs
break; //only spawn 1 new pixel per frame
}
} else { //dissolve to secondary
if (pixels[i] != SEGCOLOR(1)) {
@@ -724,14 +735,27 @@ uint16_t dissolve(uint32_t color) {
}
}
}
// fix for #4401
for (unsigned i = 0; i < SEGLEN; i++) SEGMENT.setPixelColor(i, pixels[i]);
unsigned incompletePixels = 0;
for (unsigned i = 0; i < SEGLEN; i++) {
SEGMENT.setPixelColor(i, pixels[i]); // fix for #4401
if (SEGMENT.check2) {
if (SEGENV.aux0) {
if (pixels[i] == SEGCOLOR(1)) incompletePixels++;
} else {
if (pixels[i] != SEGCOLOR(1)) incompletePixels++;
}
}
}
if (SEGENV.step > (255 - SEGMENT.speed) + 15U) {
SEGENV.aux0 = !SEGENV.aux0;
SEGENV.step = 0;
} else {
SEGENV.step++;
if (SEGMENT.check2) {
if (incompletePixels == 0)
SEGENV.step++; // only advance step once all pixels have changed
} else
SEGENV.step++;
}
return FRAMETIME;
@@ -744,7 +768,7 @@ uint16_t dissolve(uint32_t color) {
uint16_t mode_dissolve(void) {
return dissolve(SEGMENT.check1 ? SEGMENT.color_wheel(hw_random8()) : SEGCOLOR(0));
}
static const char _data_FX_MODE_DISSOLVE[] PROGMEM = "Dissolve@Repeat speed,Dissolve speed,,,,Random;!,!;!";
static const char _data_FX_MODE_DISSOLVE[] PROGMEM = "Dissolve@Repeat speed,Dissolve speed,,,,Random,Complete;!,!;!";
/*
@@ -755,7 +779,6 @@ uint16_t mode_dissolve_random(void) {
}
static const char _data_FX_MODE_DISSOLVE_RANDOM[] PROGMEM = "Dissolve Rnd@Repeat speed,Dissolve speed;,!;!";
/*
* Blinks one LED at a time.
* Inspired by www.tweaking4all.com/hardware/arduino/adruino-led-strip-effects/
@@ -777,7 +800,6 @@ uint16_t mode_sparkle(void) {
}
static const char _data_FX_MODE_SPARKLE[] PROGMEM = "Sparkle@!,,,,,,Overlay;!,!;!;;m12=0";
/*
* Lights all LEDs in the color. Flashes single col 1 pixels randomly. (List name: Sparkle Dark)
* Inspired by www.tweaking4all.com/hardware/arduino/adruino-led-strip-effects/
@@ -1752,7 +1774,6 @@ uint16_t mode_tricolor_fade(void) {
}
static const char _data_FX_MODE_TRICOLOR_FADE[] PROGMEM = "Tri Fade@!;1,2,3;!";
#ifdef WLED_PS_DONT_REPLACE_FX
/*
* Creates random comets
* Custom mode by Keith Lord: https://github.com/kitesurfer1404/WS2812FX/blob/master/src/custom/MultiComet.h
@@ -1791,7 +1812,6 @@ uint16_t mode_multi_comet(void) {
}
static const char _data_FX_MODE_MULTI_COMET[] PROGMEM = "Multi Comet@!,Fade;!,!;!;1";
#undef MAX_COMETS
#endif // WLED_PS_DONT_REPLACE_FX
/*
* Running random pixels ("Stream 2")
@@ -2118,7 +2138,7 @@ uint16_t mode_palette() {
}
static const char _data_FX_MODE_PALETTE[] PROGMEM = "Palette@Shift,Size,Rotation,,,Animate Shift,Animate Rotation,Anamorphic;;!;12;ix=112,c1=0,o1=1,o2=0,o3=1";
#ifdef WLED_PS_DONT_REPLACE_FX
#if defined(WLED_PS_DONT_REPLACE_1D_FX) || defined(WLED_PS_DONT_REPLACE_2D_FX)
// WLED limitation: Analog Clock overlay will NOT work when Fire2012 is active
// Fire2012 by Mark Kriegsman, July 2012
// as part of "Five Elements" shown here: http://youtu.be/knWiGsmgycY
@@ -2205,7 +2225,7 @@ uint16_t mode_fire_2012() {
return FRAMETIME;
}
static const char _data_FX_MODE_FIRE_2012[] PROGMEM = "Fire 2012@Cooling,Spark rate,,2D Blur,Boost;;!;1;pal=35,sx=64,ix=160,m12=1,c2=128"; // bars
#endif // WLED_PS_DONT_REPLACE_FX
#endif // WLED_PS_DONT_REPLACE_x_FX
// colored stripes pulsing at a defined Beats-Per-Minute (BPM)
uint16_t mode_bpm() {
@@ -3056,7 +3076,7 @@ uint16_t mode_bouncing_balls(void) {
}
static const char _data_FX_MODE_BOUNCINGBALLS[] PROGMEM = "Bouncing Balls@Gravity,# of balls,,,,,Overlay;!,!,!;!;1;m12=1"; //bar
#ifdef WLED_PS_DONT_REPLACE_FX
#ifdef WLED_PS_DONT_REPLACE_1D_FX
/*
* bouncing balls on a track track Effect modified from Aircoookie's bouncing balls
* Courtesy of pjhatch (https://github.com/pjhatch)
@@ -3156,7 +3176,7 @@ static uint16_t rolling_balls(void) {
return FRAMETIME;
}
static const char _data_FX_MODE_ROLLINGBALLS[] PROGMEM = "Rolling Balls@!,# of balls,,,,Collide,Overlay,Trails;!,!,!;!;1;m12=1"; //bar
#endif // WLED_PS_DONT_REPLACE_FX
#endif // WLED_PS_DONT_REPLACE_1D_FX
/*
* Sinelon stolen from FASTLED examples
@@ -3213,7 +3233,6 @@ uint16_t mode_sinelon_rainbow(void) {
}
static const char _data_FX_MODE_SINELON_RAINBOW[] PROGMEM = "Sinelon Rainbow@!,Trail;,,!;!";
// utility function that will add random glitter to SEGMENT
void glitter_base(uint8_t intensity, uint32_t col = ULTRAWHITE) {
if (intensity > hw_random8()) SEGMENT.setPixelColor(hw_random16(SEGLEN), col);
@@ -3418,7 +3437,7 @@ uint16_t mode_candle_multi()
}
static const char _data_FX_MODE_CANDLE_MULTI[] PROGMEM = "Candle Multi@!,!;!,!;!;;sx=96,ix=224,pal=0";
#ifdef WLED_PS_DONT_REPLACE_FX
#ifdef WLED_PS_DONT_REPLACE_1D_FX
/*
/ Fireworks in starburst effect
/ based on the video: https://www.reddit.com/r/arduino/comments/c3sd46/i_made_this_fireworks_effect_for_my_led_strips/
@@ -3550,9 +3569,9 @@ uint16_t mode_starburst(void) {
}
#undef STARBURST_MAX_FRAG
static const char _data_FX_MODE_STARBURST[] PROGMEM = "Fireworks Starburst@Chance,Fragments,,,,,Overlay;,!;!;;pal=11,m12=0";
#endif // WLED_PS_DONT_REPLACE_FX
#endif // WLED_PS_DONT_REPLACE_1DFX
#ifdef WLED_PS_DONT_REPLACE_FX
#if defined(WLED_PS_DONT_REPLACE_1D_FX) || defined(WLED_PS_DONT_REPLACE_2D_FX)
/*
* Exploding fireworks effect
* adapted from: http://www.anirama.com/1000leds/1d-fireworks/
@@ -3690,7 +3709,7 @@ uint16_t mode_exploding_fireworks(void)
}
#undef MAX_SPARKS
static const char _data_FX_MODE_EXPLODING_FIREWORKS[] PROGMEM = "Fireworks 1D@Gravity,Firing side;!,!;!;12;pal=11,ix=128";
#endif // WLED_PS_DONT_REPLACE_FX
#endif // WLED_PS_DONT_REPLACE_x_FX
/*
* Drip Effect
@@ -4338,7 +4357,7 @@ static const char _data_FX_MODE_CHUNCHUN[] PROGMEM = "Chunchun@!,Gap size;!,!;!"
#define SPOT_MAX_COUNT 49 //Number of simultaneous waves
#endif
#ifdef WLED_PS_DONT_REPLACE_FX
#ifdef WLED_PS_DONT_REPLACE_1D_FX
//13 bytes
typedef struct Spotlight {
float speed;
@@ -4472,7 +4491,7 @@ uint16_t mode_dancing_shadows(void)
return FRAMETIME;
}
static const char _data_FX_MODE_DANCING_SHADOWS[] PROGMEM = "Dancing Shadows@!,# of shadows;!;!";
#endif // WLED_PS_DONT_REPLACE_FX
#endif // WLED_PS_DONT_REPLACE_1D_FX
/*
Imitates a washing machine, rotating same waves forward, then pause, then backward.
@@ -4509,7 +4528,7 @@ uint16_t mode_image(void) {
// Serial.println(status);
// }
}
static const char _data_FX_MODE_IMAGE[] PROGMEM = "Image@!,;;;12;sx=128";
static const char _data_FX_MODE_IMAGE[] PROGMEM = "Image@!,Blur,;;;12;sx=128,ix=0";
/*
Blends random colors across palette
@@ -4669,7 +4688,8 @@ static const char _data_FX_MODE_TV_SIMULATOR[] PROGMEM = "TV Simulator@!,!;;!;01
/*
Aurora effect
Aurora effect by @Mazen
improved and converted to integer math by @dedehai
*/
//CONFIG
@@ -4681,140 +4701,138 @@ static const char _data_FX_MODE_TV_SIMULATOR[] PROGMEM = "TV Simulator@!,!;;!;01
#define W_MAX_SPEED 6 //Higher number, higher speed
#define W_WIDTH_FACTOR 6 //Higher number, smaller waves
//24 bytes
// fixed-point math scaling
#define AW_SHIFT 16
#define AW_SCALE (1 << AW_SHIFT) // 65536 representing 1.0
// 32 bytes
class AuroraWave {
private:
int32_t center; // scaled by AW_SCALE
uint32_t ageFactor_cached; // cached age factor scaled by AW_SCALE
uint16_t ttl;
CRGB basecolor;
float basealpha;
uint16_t age;
uint16_t width;
float center;
uint16_t basealpha; // scaled by AW_SCALE
uint16_t speed_factor; // scaled by AW_SCALE
int16_t wave_start; // wave start LED index
int16_t wave_end; // wave end LED index
bool goingleft;
float speed_factor;
bool alive = true;
CRGBW basecolor;
public:
void init(uint32_t segment_length, CRGB color) {
void init(uint32_t segment_length, CRGBW color) {
ttl = hw_random16(500, 1501);
basecolor = color;
basealpha = hw_random8(60, 101) / (float)100;
basealpha = hw_random8(60, 100) * AW_SCALE / 100; // 0-99% note: if using 100% there is risk of integer overflow
age = 0;
width = hw_random16(segment_length / 20, segment_length / W_WIDTH_FACTOR); //half of width to make math easier
if (!width) width = 1;
center = hw_random8(101) / (float)100 * segment_length;
goingleft = hw_random8(0, 2) == 0;
speed_factor = (hw_random8(10, 31) / (float)100 * W_MAX_SPEED / 255);
width = hw_random16(segment_length / 20, segment_length / W_WIDTH_FACTOR) + 1;
center = (((uint32_t)hw_random8(101) << AW_SHIFT) / 100) * segment_length; // 0-100%
goingleft = hw_random8() & 0x01; // 50/50 chance
speed_factor = (((uint32_t)hw_random8(10, 31) * W_MAX_SPEED) << AW_SHIFT) / (100 * 255);
alive = true;
}
CRGB getColorForLED(int ledIndex) {
if(ledIndex < center - width || ledIndex > center + width) return 0; //Position out of range of this wave
CRGB rgb;
//Offset of this led from center of wave
//The further away from the center, the dimmer the LED
float offset = ledIndex - center;
if (offset < 0) offset = -offset;
float offsetFactor = offset / width;
//The age of the wave determines it brightness.
//At half its maximum age it will be the brightest.
float ageFactor = 0.1;
if((float)age / ttl < 0.5) {
ageFactor = (float)age / (ttl / 2);
void updateCachedValues() {
uint32_t half_ttl = ttl >> 1;
if (age < half_ttl) {
ageFactor_cached = ((uint32_t)age << AW_SHIFT) / half_ttl;
} else {
ageFactor = (float)(ttl - age) / ((float)ttl * 0.5);
ageFactor_cached = ((uint32_t)(ttl - age) << AW_SHIFT) / half_ttl;
}
if (ageFactor_cached >= AW_SCALE) ageFactor_cached = AW_SCALE - 1; // prevent overflow
//Calculate color based on above factors and basealpha value
float factor = (1 - offsetFactor) * ageFactor * basealpha;
rgb.r = basecolor.r * factor;
rgb.g = basecolor.g * factor;
rgb.b = basecolor.b * factor;
uint32_t center_led = center >> AW_SHIFT;
wave_start = (int16_t)center_led - (int16_t)width;
wave_end = (int16_t)center_led + (int16_t)width;
}
CRGBW getColorForLED(int ledIndex) {
// linear brightness falloff from center to edge of wave
if (ledIndex < wave_start || ledIndex > wave_end) return 0;
int32_t ledIndex_scaled = (int32_t)ledIndex << AW_SHIFT;
int32_t offset = ledIndex_scaled - center;
if (offset < 0) offset = -offset;
uint32_t offsetFactor = offset / width; // scaled by AW_SCALE
if (offsetFactor > AW_SCALE) return 0; // outside of wave
uint32_t brightness_factor = (AW_SCALE - offsetFactor);
brightness_factor = (brightness_factor * ageFactor_cached) >> AW_SHIFT;
brightness_factor = (brightness_factor * basealpha) >> AW_SHIFT;
CRGBW rgb;
rgb.r = (basecolor.r * brightness_factor) >> AW_SHIFT;
rgb.g = (basecolor.g * brightness_factor) >> AW_SHIFT;
rgb.b = (basecolor.b * brightness_factor) >> AW_SHIFT;
rgb.w = (basecolor.w * brightness_factor) >> AW_SHIFT;
return rgb;
};
//Change position and age of wave
//Determine if its sill "alive"
//Determine if its still "alive"
void update(uint32_t segment_length, uint32_t speed) {
if(goingleft) {
center -= speed_factor * speed;
} else {
center += speed_factor * speed;
}
int32_t step = speed_factor * speed;
center += goingleft ? -step : step;
age++;
if(age > ttl) {
if (age > ttl) {
alive = false;
} else {
if(goingleft) {
if(center + width < 0) {
alive = false;
}
} else {
if(center - width > segment_length) {
alive = false;
}
}
uint32_t width_scaled = (uint32_t)width << AW_SHIFT;
uint32_t segment_length_scaled = segment_length << AW_SHIFT;
if (goingleft) {
if (center < - (int32_t)width_scaled) {
alive = false;
}
} else {
if (center > (int32_t)segment_length_scaled + (int32_t)width_scaled) {
alive = false;
}
}
}
};
bool stillAlive() {
return alive;
};
bool stillAlive() { return alive; }
};
uint16_t mode_aurora(void) {
AuroraWave* waves;
SEGENV.aux1 = map(SEGMENT.intensity, 0, 255, 2, W_MAX_COUNT); // aux1 = Wavecount
if(!SEGENV.allocateData(sizeof(AuroraWave) * SEGENV.aux1)) { // 20 on ESP32, 9 on ESP8266
return mode_static(); //allocation failed
if (!SEGENV.allocateData(sizeof(AuroraWave) * SEGENV.aux1)) {
return mode_static();
}
waves = reinterpret_cast<AuroraWave*>(SEGENV.data);
if(SEGENV.call == 0) {
for (int i = 0; i < SEGENV.aux1; i++) {
waves[i].init(SEGLEN, CRGB(SEGMENT.color_from_palette(hw_random8(), false, false, hw_random8(0, 3))));
}
}
// note: on first call, SEGENV.data is zero -> all waves are dead and will be initialized
for (int i = 0; i < SEGENV.aux1; i++) {
//Update values of wave
waves[i].update(SEGLEN, SEGMENT.speed);
if(!(waves[i].stillAlive())) {
//If a wave dies, reinitialize it starts over.
waves[i].init(SEGLEN, CRGB(SEGMENT.color_from_palette(hw_random8(), false, false, hw_random8(0, 3))));
if (!(waves[i].stillAlive())) {
waves[i].init(SEGLEN, SEGMENT.color_from_palette(hw_random8(), false, false, hw_random8(0, 3)));
}
waves[i].updateCachedValues();
}
uint8_t backlight = 1; //dimmer backlight if less active colors
uint8_t backlight = 0; // note: original code used 1, with inverse gamma applied background would never be black
if (SEGCOLOR(0)) backlight++;
if (SEGCOLOR(1)) backlight++;
if (SEGCOLOR(2)) backlight++;
//Loop through LEDs to determine color
backlight = gamma8inv(backlight); // preserve backlight when using gamma correction
for (unsigned i = 0; i < SEGLEN; i++) {
CRGB mixedRgb = CRGB(backlight, backlight, backlight);
CRGBW mixedRgb = CRGBW(backlight, backlight, backlight);
//For each LED we must check each wave if it is "active" at this position.
//If there are multiple waves active on a LED we multiply their values.
for (int j = 0; j < SEGENV.aux1; j++) {
CRGB rgb = waves[j].getColorForLED(i);
if(rgb != CRGB(0)) {
mixedRgb += rgb;
}
for (int j = 0; j < SEGENV.aux1; j++) {
CRGBW rgb = waves[j].getColorForLED(i);
mixedRgb = color_add(mixedRgb, rgb); // sum all waves influencing this pixel
}
SEGMENT.setPixelColor(i, mixedRgb[0], mixedRgb[1], mixedRgb[2]);
SEGMENT.setPixelColor(i, mixedRgb);
}
return FRAMETIME;
}
static const char _data_FX_MODE_AURORA[] PROGMEM = "Aurora@!,!;1,2,3;!;;sx=24,pal=50";
// WLED-SR effects
@@ -5196,112 +5214,162 @@ static const char _data_FX_MODE_2DFRIZZLES[] PROGMEM = "Frizzles@X frequency,Y f
///////////////////////////////////////////
// 2D Cellular Automata Game of life //
///////////////////////////////////////////
typedef struct ColorCount {
CRGB color;
int8_t count;
} colorCount;
typedef struct Cell {
uint8_t alive : 1, faded : 1, toggleStatus : 1, edgeCell: 1, oscillatorCheck : 1, spaceshipCheck : 1, unused : 2;
} Cell;
uint16_t mode_2Dgameoflife(void) { // Written by Ewoud Wijma, inspired by https://natureofcode.com/book/chapter-7-cellular-automata/ and https://github.com/DougHaber/nlife-color
uint16_t mode_2Dgameoflife(void) { // Written by Ewoud Wijma, inspired by https://natureofcode.com/book/chapter-7-cellular-automata/
// and https://github.com/DougHaber/nlife-color , Modified By: Brandon Butler
if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up
const int cols = SEG_W, rows = SEG_H;
const unsigned maxIndex = cols * rows;
const int cols = SEG_W;
const int rows = SEG_H;
const auto XY = [&](int x, int y) { return (x%cols) + (y%rows) * cols; };
const unsigned dataSize = sizeof(CRGB) * SEGMENT.length(); // using width*height prevents reallocation if mirroring is enabled
const int crcBufferLen = 2; //(SEGMENT.width() + SEGMENT.height())*71/100; // roughly sqrt(2)/2 for better repetition detection (Ewowi)
if (!SEGENV.allocateData(SEGMENT.length() * sizeof(Cell))) return mode_static(); // allocation failed
if (!SEGENV.allocateData(dataSize + sizeof(uint16_t)*crcBufferLen)) return mode_static(); //allocation failed
CRGB *prevLeds = reinterpret_cast<CRGB*>(SEGENV.data);
uint16_t *crcBuffer = reinterpret_cast<uint16_t*>(SEGENV.data + dataSize);
Cell *cells = reinterpret_cast<Cell*> (SEGENV.data);
CRGB backgroundColor = SEGCOLOR(1);
uint16_t& generation = SEGENV.aux0, &gliderLength = SEGENV.aux1; // rename aux variables for clarity
bool mutate = SEGMENT.check3;
uint8_t blur = map(SEGMENT.custom1, 0, 255, 255, 4);
if (SEGENV.call == 0 || strip.now - SEGMENT.step > 3000) {
SEGENV.step = strip.now;
SEGENV.aux0 = 0;
uint32_t bgColor = SEGCOLOR(1);
uint32_t birthColor = SEGMENT.color_from_palette(128, false, PALETTE_SOLID_WRAP, 255);
//give the leds random state and colors (based on intensity, colors from palette or all posible colors are chosen)
for (int x = 0; x < cols; x++) for (int y = 0; y < rows; y++) {
unsigned state = hw_random8()%2;
if (state == 0)
SEGMENT.setPixelColorXY(x,y, backgroundColor);
else
SEGMENT.setPixelColorXY(x,y, SEGMENT.color_from_palette(hw_random8(), false, PALETTE_SOLID_WRAP, 255));
bool setup = SEGENV.call == 0;
if (setup) {
// Calculate glider length LCM(rows,cols)*4 once
unsigned a = rows, b = cols;
while (b) { unsigned t = b; b = a % b; a = t; }
gliderLength = (cols * rows / a) << 2;
}
if (abs(long(strip.now) - long(SEGENV.step)) > 2000) SEGENV.step = 0; // Timebase jump fix
bool paused = SEGENV.step > strip.now;
// Setup New Game of Life
if ((!paused && generation == 0) || setup) {
SEGENV.step = strip.now + 1280; // show initial state for 1.28 seconds
generation = 1;
paused = true;
//Setup Grid
memset(cells, 0, maxIndex * sizeof(Cell));
for (unsigned i = 0; i < maxIndex; i++) {
bool isAlive = !hw_random8(3); // ~33%
cells[i].alive = isAlive;
cells[i].faded = !isAlive;
unsigned x = i % cols, y = i / cols;
cells[i].edgeCell = (x == 0 || x == cols-1 || y == 0 || y == rows-1);
SEGMENT.setPixelColor(i, isAlive ? SEGMENT.color_from_palette(hw_random8(), false, PALETTE_SOLID_WRAP, 0) : bgColor);
}
}
for (int y = 0; y < rows; y++) for (int x = 0; x < cols; x++) prevLeds[XY(x,y)] = CRGB::Black;
memset(crcBuffer, 0, sizeof(uint16_t)*crcBufferLen);
} else if (strip.now - SEGENV.step < FRAMETIME_FIXED * (uint32_t)map(SEGMENT.speed,0,255,64,4)) {
// update only when appropriate time passes (in 42 FPS slots)
if (paused || (strip.now - SEGENV.step < 1000 / map(SEGMENT.speed,0,255,1,42))) {
// Redraw if paused or between updates to remove blur
for (unsigned i = maxIndex; i--; ) {
if (!cells[i].alive) {
uint32_t cellColor = SEGMENT.getPixelColor(i);
if (cellColor != bgColor) {
uint32_t newColor;
bool needsColor = false;
if (cells[i].faded) { newColor = bgColor; needsColor = true; }
else {
uint32_t blended = color_blend(cellColor, bgColor, 2);
if (blended == cellColor) { blended = bgColor; cells[i].faded = 1; }
newColor = blended; needsColor = true;
}
if (needsColor) SEGMENT.setPixelColor(i, newColor);
}
}
}
return FRAMETIME;
}
//copy previous leds (save previous generation)
//NOTE: using lossy getPixelColor() is a benefit as endlessly repeating patterns will eventually fade out causing a reset
for (int x = 0; x < cols; x++) for (int y = 0; y < rows; y++) prevLeds[XY(x,y)] = SEGMENT.getPixelColorXY(x,y);
// Repeat detection
bool updateOscillator = generation % 16 == 0;
bool updateSpaceship = gliderLength && generation % gliderLength == 0;
bool repeatingOscillator = true, repeatingSpaceship = true, emptyGrid = true;
//calculate new leds
for (int x = 0; x < cols; x++) for (int y = 0; y < rows; y++) {
unsigned cIndex = maxIndex-1;
for (unsigned y = rows; y--; ) for (unsigned x = cols; x--; cIndex--) {
Cell& cell = cells[cIndex];
colorCount colorsCount[9]; // count the different colors in the 3*3 matrix
for (int i=0; i<9; i++) colorsCount[i] = {backgroundColor, 0}; // init colorsCount
if (cell.alive) emptyGrid = false;
if (cell.oscillatorCheck != cell.alive) repeatingOscillator = false;
if (cell.spaceshipCheck != cell.alive) repeatingSpaceship = false;
if (updateOscillator) cell.oscillatorCheck = cell.alive;
if (updateSpaceship) cell.spaceshipCheck = cell.alive;
// iterate through neighbors and count them and their different colors
int neighbors = 0;
for (int i = -1; i <= 1; i++) for (int j = -1; j <= 1; j++) { // iterate through 3*3 matrix
if (i==0 && j==0) continue; // ignore itself
// wrap around segment
int xx = x+i, yy = y+j;
if (x+i < 0) xx = cols-1; else if (x+i >= cols) xx = 0;
if (y+j < 0) yy = rows-1; else if (y+j >= rows) yy = 0;
unsigned xy = XY(xx, yy); // previous cell xy to check
// count different neighbours and colors
if (prevLeds[xy] != backgroundColor) {
neighbors++;
bool colorFound = false;
int k;
for (k=0; k<9 && colorsCount[k].count != 0; k++)
if (colorsCount[k].color == prevLeds[xy]) {
colorsCount[k].count++;
colorFound = true;
}
if (!colorFound) colorsCount[k] = {prevLeds[xy], 1}; //add new color found in the array
unsigned neighbors = 0, aliveParents = 0, parentIdx[3];
// Count alive neighbors
for (int i = -1; i <= 1; i++) for (int j = -1; j <= 1; j++) if (i || j) {
int nX = x + j, nY = y + i;
if (cell.edgeCell) {
nX = (nX + cols) % cols;
nY = (nY + rows) % rows;
}
unsigned nIndex = nX + nY * cols;
Cell& neighbor = cells[nIndex];
if (neighbor.alive) {
neighbors++;
if (!neighbor.toggleStatus && neighbors < 4) { // Alive and not dying
parentIdx[aliveParents++] = nIndex;
}
}
} // i,j
// Rules of Life
uint32_t col = uint32_t(prevLeds[XY(x,y)]) & 0x00FFFFFF; // uint32_t operator returns RGBA, we want RGBW -> cut off "alpha" byte
uint32_t bgc = RGBW32(backgroundColor.r, backgroundColor.g, backgroundColor.b, 0);
if ((col != bgc) && (neighbors < 2)) SEGMENT.setPixelColorXY(x,y, bgc); // Loneliness
else if ((col != bgc) && (neighbors > 3)) SEGMENT.setPixelColorXY(x,y, bgc); // Overpopulation
else if ((col == bgc) && (neighbors == 3)) { // Reproduction
// find dominant color and assign it to a cell
colorCount dominantColorCount = {backgroundColor, 0};
for (int i=0; i<9 && colorsCount[i].count != 0; i++)
if (colorsCount[i].count > dominantColorCount.count) dominantColorCount = colorsCount[i];
// assign the dominant color w/ a bit of randomness to avoid "gliders"
if (dominantColorCount.count > 0 && hw_random8(128)) SEGMENT.setPixelColorXY(x,y, dominantColorCount.color);
} else if ((col == bgc) && (neighbors == 2) && !hw_random8(128)) { // Mutation
SEGMENT.setPixelColorXY(x,y, SEGMENT.color_from_palette(hw_random8(), false, PALETTE_SOLID_WRAP, 255));
}
// else do nothing!
} //x,y
// calculate CRC16 of leds
uint16_t crc = crc16((const unsigned char*)prevLeds, dataSize);
// check if we had same CRC and reset if needed
bool repetition = false;
for (int i=0; i<crcBufferLen && !repetition; i++) repetition = (crc == crcBuffer[i]); // (Ewowi)
// same CRC would mean image did not change or was repeating itself
if (!repetition) SEGENV.step = strip.now; //if no repetition avoid reset
// remember CRCs across frames
crcBuffer[SEGENV.aux0] = crc;
++SEGENV.aux0 %= crcBufferLen;
uint32_t newColor;
bool needsColor = false;
if (cell.alive && (neighbors < 2 || neighbors > 3)) { // Loneliness or Overpopulation
cell.toggleStatus = 1;
if (blur == 255) cell.faded = 1;
newColor = cell.faded ? bgColor : color_blend(SEGMENT.getPixelColor(cIndex), bgColor, blur);
needsColor = true;
}
else if (!cell.alive) {
byte mutationRoll = mutate ? hw_random8(128) : 1; // if 0: 3 neighbor births fail and 2 neighbor births mutate
if ((neighbors == 3 && mutationRoll) || (mutate && neighbors == 2 && !mutationRoll)) { // Reproduction or Mutation
cell.toggleStatus = 1;
cell.faded = 0;
if (aliveParents) {
// Set color based on random neighbor
unsigned parentIndex = parentIdx[random8(aliveParents)];
birthColor = SEGMENT.getPixelColor(parentIndex);
}
newColor = birthColor;
needsColor = true;
}
else if (!cell.faded) {// No change, fade dead cells
uint32_t cellColor = SEGMENT.getPixelColor(cIndex);
uint32_t blended = color_blend(cellColor, bgColor, blur);
if (blended == cellColor) { blended = bgColor; cell.faded = 1; }
newColor = blended;
needsColor = true;
}
}
if (needsColor) SEGMENT.setPixelColor(cIndex, newColor);
}
// Loop through cells, if toggle, swap alive status
for (unsigned i = maxIndex; i--; ) {
cells[i].alive ^= cells[i].toggleStatus;
cells[i].toggleStatus = 0;
}
if (repeatingOscillator || repeatingSpaceship || emptyGrid) {
generation = 0; // reset on next call
SEGENV.step += 1024; // pause final generation for ~1 second
}
else {
++generation;
SEGENV.step = strip.now;
}
return FRAMETIME;
} // mode_2Dgameoflife()
static const char _data_FX_MODE_2DGAMEOFLIFE[] PROGMEM = "Game Of Life@!;!,!;!;2";
static const char _data_FX_MODE_2DGAMEOFLIFE[] PROGMEM = "Game Of Life@!,,Blur,,,,,Mutation;!,!;!;2;pal=11,sx=128";
/////////////////////////
@@ -5984,7 +6052,7 @@ uint16_t mode_2Dcrazybees(void) {
static const char _data_FX_MODE_2DCRAZYBEES[] PROGMEM = "Crazy Bees@!,Blur,,,,Smear;;!;2;pal=11,ix=0";
#undef MAX_BEES
#ifdef WLED_PS_DONT_REPLACE_FX
#ifdef WLED_PS_DONT_REPLACE_2D_FX
/////////////////////////
// 2D Ghost Rider //
/////////////////////////
@@ -6172,7 +6240,7 @@ uint16_t mode_2Dfloatingblobs(void) {
}
static const char _data_FX_MODE_2DBLOBS[] PROGMEM = "Blobs@!,# blobs,Blur,Trail;!;!;2;c1=8";
#undef MAX_BLOBS
#endif // WLED_PS_DONT_REPLACE_FX
#endif // WLED_PS_DONT_REPLACE_2D_FX
////////////////////////////
// 2D Scrolling text //
@@ -9628,11 +9696,11 @@ uint16_t mode_particleFireworks1D(void) {
PartSys->sources[0].sourceFlags.custom1 = 0; //flag used for rocket state
PartSys->sources[0].source.hue = hw_random16(); // different color for each launch
PartSys->sources[0].var = 10; // emit variation
PartSys->sources[0].v = -10; // emit speed
PartSys->sources[0].minLife = 30;
PartSys->sources[0].maxLife = SEGMENT.check2 ? 400 : 60;
PartSys->sources[0].source.x = 0; // start from bottom
PartSys->sources[0].var = 10 * SEGMENT.check2; // emit variation, 0 if trail mode is off
PartSys->sources[0].v = -10 * SEGMENT.check2; // emit speed, 0 if trail mode is off
PartSys->sources[0].minLife = 180;
PartSys->sources[0].maxLife = SEGMENT.check2 ? 700 : 240; // exhaust particle life
PartSys->sources[0].source.x = SEGENV.aux0 * PartSys->maxX; // start from bottom or top
uint32_t speed = sqrt((gravity * ((PartSys->maxX >> 2) + hw_random16(PartSys->maxX >> 1))) >> 4); // set speed such that rocket explods in frame
PartSys->sources[0].source.vx = min(speed, (uint32_t)127);
PartSys->sources[0].source.ttl = 4000;
@@ -9642,7 +9710,7 @@ uint16_t mode_particleFireworks1D(void) {
if (SEGENV.aux0) { // inverted rockets launch from end
PartSys->sources[0].sourceFlags.reversegrav = true;
PartSys->sources[0].source.x = PartSys->maxX; // start from top
//PartSys->sources[0].source.x = PartSys->maxX; // start from top
PartSys->sources[0].source.vx = -PartSys->sources[0].source.vx; // revert direction
PartSys->sources[0].v = -PartSys->sources[0].v; // invert exhaust emit speed
}
@@ -9661,18 +9729,20 @@ uint16_t mode_particleFireworks1D(void) {
uint32_t rocketheight = SEGENV.aux0 ? PartSys->maxX - PartSys->sources[0].source.x : PartSys->sources[0].source.x;
if (currentspeed < 0 && PartSys->sources[0].source.ttl > 50) // reached apogee
PartSys->sources[0].source.ttl = min((uint32_t)50, rocketheight >> (PS_P_RADIUS_SHIFT_1D + 3)); // alive for a few more frames
PartSys->sources[0].source.ttl = 50 - gravity;// min((uint32_t)50, 15 + (rocketheight >> (PS_P_RADIUS_SHIFT_1D + 3))); // alive for a few more frames
if (PartSys->sources[0].source.ttl < 2) { // explode
PartSys->sources[0].sourceFlags.custom1 = 1; // set standby state
PartSys->sources[0].var = 5 + ((((PartSys->maxX >> 1) + rocketheight) * (200 + SEGMENT.intensity)) / (PartSys->maxX << 2)); // set explosion particle speed
PartSys->sources[0].minLife = 600;
PartSys->sources[0].maxLife = 1300;
PartSys->sources[0].var = 5 + ((((PartSys->maxX >> 1) + rocketheight) * (20 + (SEGMENT.intensity << 1))) / (PartSys->maxX << 2)); // set explosion particle speed
PartSys->sources[0].minLife = 1200;
PartSys->sources[0].maxLife = 2600;
PartSys->sources[0].source.ttl = 100 + hw_random16(64 - (SEGMENT.speed >> 2)); // standby time til next launch
PartSys->sources[0].sat = SEGMENT.custom3 < 16 ? 10 + (SEGMENT.custom3 << 4) : 255; //color saturation
PartSys->sources[0].size = SEGMENT.check3 ? hw_random16(SEGMENT.intensity) : 0; // random particle size in explosion
uint32_t explosionsize = 8 + (PartSys->maxXpixel >> 2) + (PartSys->sources[0].source.x >> (PS_P_RADIUS_SHIFT_1D - 1));
explosionsize += hw_random16((explosionsize * SEGMENT.intensity) >> 8);
PartSys->setColorByAge(false); // disable
PartSys->setColorByPosition(false); // disable
for (uint32_t e = 0; e < explosionsize; e++) { // emit explosion particles
int idx = PartSys->sprayEmit(PartSys->sources[0]); // emit a particle
if(SEGMENT.custom3 > 23) {
@@ -9692,16 +9762,16 @@ uint16_t mode_particleFireworks1D(void) {
}
}
}
if ((SEGMENT.call & 0x01) == 0 && PartSys->sources[0].sourceFlags.custom1 == false && PartSys->sources[0].source.ttl > 50) // every second frame and not in standby and not about to explode
if ((SEGMENT.call & 0x01) == 0 && PartSys->sources[0].sourceFlags.custom1 == false) // every second frame and not in standby
PartSys->sprayEmit(PartSys->sources[0]); // emit exhaust particle
if ((SEGMENT.call & 0x03) == 0) // every fourth frame
PartSys->applyFriction(1); // apply friction to all particles
PartSys->update(); // update and render
for (uint32_t i = 0; i < PartSys->usedParticles; i++) {
if (PartSys->particles[i].ttl > 10) PartSys->particles[i].ttl -= 10; //ttl is linked to brightness, this allows to use higher brightness but still a short spark lifespan
if (PartSys->particles[i].ttl > 20) PartSys->particles[i].ttl -= 20; //ttl is linked to brightness, this allows to use higher brightness but still a short spark lifespan
else PartSys->particles[i].ttl = 0;
}
return FRAMETIME;
@@ -10822,16 +10892,18 @@ void WS2812FX::setupEffectData() {
addEffect(FX_MODE_SPOTS, &mode_spots, _data_FX_MODE_SPOTS);
addEffect(FX_MODE_SPOTS_FADE, &mode_spots_fade, _data_FX_MODE_SPOTS_FADE);
addEffect(FX_MODE_COMET, &mode_comet, _data_FX_MODE_COMET);
#ifdef WLED_PS_DONT_REPLACE_FX
addEffect(FX_MODE_MULTI_COMET, &mode_multi_comet, _data_FX_MODE_MULTI_COMET);
addEffect(FX_MODE_ROLLINGBALLS, &rolling_balls, _data_FX_MODE_ROLLINGBALLS);
#if defined(WLED_PS_DONT_REPLACE_1D_FX) || defined(WLED_PS_DONT_REPLACE_2D_FX)
addEffect(FX_MODE_FIRE_2012, &mode_fire_2012, _data_FX_MODE_FIRE_2012);
addEffect(FX_MODE_EXPLODING_FIREWORKS, &mode_exploding_fireworks, _data_FX_MODE_EXPLODING_FIREWORKS);
#endif
addEffect(FX_MODE_SPARKLE, &mode_sparkle, _data_FX_MODE_SPARKLE);
addEffect(FX_MODE_GLITTER, &mode_glitter, _data_FX_MODE_GLITTER);
addEffect(FX_MODE_SOLID_GLITTER, &mode_solid_glitter, _data_FX_MODE_SOLID_GLITTER);
addEffect(FX_MODE_MULTI_COMET, &mode_multi_comet, _data_FX_MODE_MULTI_COMET);
#ifdef WLED_PS_DONT_REPLACE_1D_FX
addEffect(FX_MODE_ROLLINGBALLS, &rolling_balls, _data_FX_MODE_ROLLINGBALLS);
addEffect(FX_MODE_STARBURST, &mode_starburst, _data_FX_MODE_STARBURST);
addEffect(FX_MODE_DANCING_SHADOWS, &mode_dancing_shadows, _data_FX_MODE_DANCING_SHADOWS);
addEffect(FX_MODE_FIRE_2012, &mode_fire_2012, _data_FX_MODE_FIRE_2012);
addEffect(FX_MODE_EXPLODING_FIREWORKS, &mode_exploding_fireworks, _data_FX_MODE_EXPLODING_FIREWORKS);
#endif
addEffect(FX_MODE_CANDLE, &mode_candle, _data_FX_MODE_CANDLE);
addEffect(FX_MODE_BOUNCINGBALLS, &mode_bouncing_balls, _data_FX_MODE_BOUNCINGBALLS);
@@ -10895,7 +10967,7 @@ void WS2812FX::setupEffectData() {
addEffect(FX_MODE_2DSPACESHIPS, &mode_2Dspaceships, _data_FX_MODE_2DSPACESHIPS);
addEffect(FX_MODE_2DCRAZYBEES, &mode_2Dcrazybees, _data_FX_MODE_2DCRAZYBEES);
#ifdef WLED_PS_DONT_REPLACE_FX
#ifdef WLED_PS_DONT_REPLACE_2D_FX
addEffect(FX_MODE_2DGHOSTRIDER, &mode_2Dghostrider, _data_FX_MODE_2DGHOSTRIDER);
addEffect(FX_MODE_2DBLOBS, &mode_2Dfloatingblobs, _data_FX_MODE_2DBLOBS);
#endif

View File

@@ -17,6 +17,7 @@
// note: matrix may be comprised of multiple panels each with different orientation
// but ledmap takes care of that. ledmap is constructed upon initialization
// so matrix should disable regular ledmap processing
// WARNING: effect drawing has to be suspended (strip.suspend()) or must be called from loop() context
void WS2812FX::setUpMatrix() {
#ifndef WLED_DISABLE_2D
// isMatrix is set in cfg.cpp or set.cpp
@@ -45,12 +46,12 @@ void WS2812FX::setUpMatrix() {
return;
}
suspend();
waitForIt();
customMappingSize = 0; // prevent use of mapping if anything goes wrong
d_free(customMappingTable);
// Segment::maxWidth and Segment::maxHeight are set according to panel layout
// and the product will include at least all leds in matrix
// if actual LEDs are more, getLengthTotal() will return correct number of LEDs
customMappingTable = static_cast<uint16_t*>(d_malloc(sizeof(uint16_t)*getLengthTotal())); // prefer to not use SPI RAM
if (customMappingTable) {
@@ -113,7 +114,6 @@ void WS2812FX::setUpMatrix() {
// delete gap array as we no longer need it
p_free(gapTable);
resume();
#ifdef WLED_DEBUG
DEBUG_PRINT(F("Matrix ledmap:"));

View File

@@ -1681,12 +1681,17 @@ void WS2812FX::setTransitionMode(bool t) {
resume();
}
// wait until frame is over (service() has finished or time for 1 frame has passed; yield() crashes on 8266)
// wait until frame is over (service() has finished or time for 2 frames have passed; yield() crashes on 8266)
// the latter may, in rare circumstances, lead to incorrectly assuming strip is done servicing but will not block
// other processing "indefinitely"
// rare circumstances are: setting FPS to high number (i.e. 120) and have very slow effect that will need more
// time than 2 * _frametime (1000/FPS) to draw content
void WS2812FX::waitForIt() {
unsigned long maxWait = millis() + getFrameTime() + 100; // TODO: this needs a proper fix for timeout!
while (isServicing() && maxWait > millis()) delay(1);
unsigned long waitStart = millis();
unsigned long maxWait = 2*getFrameTime() + 100; // TODO: this needs a proper fix for timeout! see #4779
while (isServicing() && (millis() - waitStart < maxWait)) delay(1); // safe even when millis() rolls over
#ifdef WLED_DEBUG
if (millis() >= maxWait) DEBUG_PRINTLN(F("Waited for strip to finish servicing."));
if (millis()-waitStart >= maxWait) DEBUG_PRINTLN(F("Waited for strip to finish servicing."));
#endif
};
@@ -1810,7 +1815,11 @@ Segment& WS2812FX::getSegment(unsigned id) {
return _segments[id >= _segments.size() ? getMainSegmentId() : id]; // vectors
}
// WARNING: resetSegments(), makeAutoSegments() and fixInvalidSegments() must not be called while
// strip is being serviced (strip.service()), you must call suspend prior if changing segments outside
// loop() context
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);
_segments.shrink_to_fit(); // just in case ...
@@ -1818,6 +1827,7 @@ void WS2812FX::resetSegments() {
}
void WS2812FX::makeAutoSegments(bool forceReset) {
if (isServicing()) return;
if (autoSegments) { //make one segment per bus
unsigned segStarts[MAX_NUM_SEGMENTS] = {0};
unsigned segStops [MAX_NUM_SEGMENTS] = {0};
@@ -1889,6 +1899,7 @@ void WS2812FX::makeAutoSegments(bool forceReset) {
}
void WS2812FX::fixInvalidSegments() {
if (isServicing()) return;
//make sure no segment is longer than total (sanity check)
for (size_t i = getSegmentsNum()-1; i > 0; i--) {
if (isMatrix) {
@@ -1951,6 +1962,7 @@ void WS2812FX::printSize() {
// load custom mapping table from JSON file (called from finalizeInit() or deserializeState())
// if this is a matrix set-up and default ledmap.json file does not exist, create mapping table using setUpMatrix() from panel information
// WARNING: effect drawing has to be suspended (strip.suspend()) or must be called from loop() context
bool WS2812FX::deserializeMap(unsigned n) {
char fileName[32];
strcpy_P(fileName, PSTR("/ledmap"));
@@ -1980,15 +1992,13 @@ bool WS2812FX::deserializeMap(unsigned n) {
} else
DEBUG_PRINTF_P(PSTR("Reading LED map from %s\n"), fileName);
suspend();
waitForIt();
JsonObject root = pDoc->as<JsonObject>();
// if we are loading default ledmap (at boot) set matrix width and height from the ledmap (compatible with WLED MM ledmaps)
if (n == 0 && (!root[F("width")].isNull() || !root[F("height")].isNull())) {
Segment::maxWidth = min(max(root[F("width")].as<int>(), 1), 255);
Segment::maxHeight = min(max(root[F("height")].as<int>(), 1), 255);
isMatrix = true;
DEBUG_PRINTF_P(PSTR("LED map width=%d, height=%d\n"), Segment::maxWidth, Segment::maxHeight);
}
d_free(customMappingTable);
@@ -2012,9 +2022,9 @@ bool WS2812FX::deserializeMap(unsigned n) {
} while (i < 32);
if (!foundDigit) break;
int index = atoi(number);
if (index < 0 || index > 16384) index = 0xFFFF;
if (index < 0 || index > 65535) index = 0xFFFF; // prevent integer wrap around
customMappingTable[customMappingSize++] = index;
if (customMappingSize > getLengthTotal()) break;
if (customMappingSize >= getLengthTotal()) break;
} else break; // there was nothing to read, stop
}
currentLedmap = n;
@@ -2024,7 +2034,7 @@ bool WS2812FX::deserializeMap(unsigned n) {
DEBUG_PRINT(F("Loaded ledmap:"));
for (unsigned i=0; i<customMappingSize; i++) {
if (!(i%Segment::maxWidth)) DEBUG_PRINTLN();
DEBUG_PRINTF_P(PSTR("%4d,"), customMappingTable[i]);
DEBUG_PRINTF_P(PSTR("%4d,"), customMappingTable[i] < 0xFFFFU ? customMappingTable[i] : -1);
}
DEBUG_PRINTLN();
#endif
@@ -2040,8 +2050,6 @@ bool WS2812FX::deserializeMap(unsigned n) {
DEBUG_PRINTLN(F("ERROR LED map allocation error."));
}
resume();
releaseJSONBufferLock();
return (customMappingSize > 0);
}

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,40 +290,39 @@ 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
@@ -332,25 +334,25 @@ void handleButton()
} 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);
}
}

View File

@@ -345,97 +345,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
@@ -777,6 +771,10 @@ bool verifyConfig() {
return validateJsonFile(s_cfg_json);
}
bool configBackupExists() {
return checkBackupExists(s_cfg_json);
}
// rename config file and reboot
// if the cfg file doesn't exist, such as after a reset, do nothing
void resetConfig() {
@@ -1012,15 +1010,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;

View File

@@ -102,9 +102,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

View File

@@ -116,3 +116,62 @@ function uploadFile(fileObj, name) {
fileObj.value = '';
return false;
}
// connect to WebSocket, use parent WS or open new
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
return ws;
}
// send LED colors to ESP using WebSocket and DDP protocol (RGB)
// ws: WebSocket object
// start: start pixel index
// len: number of pixels to send
// colors: Uint8Array with RGB values (3*len bytes)
function sendDDP(ws, start, len, colors) {
if (!colors || colors.length < len * 3) return false; // not enough color data
let maxDDPpx = 472; // must fit into one WebSocket frame of 1428 bytes, DDP header is 10+1 bytes -> 472 RGB pixels
//let maxDDPpx = 172; // ESP8266: must fit into one WebSocket frame of 528 bytes -> 172 RGB pixels TODO: add support for ESP8266?
if (!ws || ws.readyState !== WebSocket.OPEN) return false;
// send in chunks of maxDDPpx
for (let i = 0; i < len; i += maxDDPpx) {
let cnt = Math.min(maxDDPpx, len - i);
let off = (start + i) * 3; // DDP pixel offset in bytes
let dLen = cnt * 3;
let cOff = i * 3; // offset in color buffer
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[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;
pkt[7] = (off >> 8) & 255;
pkt[8] = off & 255;
pkt[9] = (dLen >> 8) & 255; // DDP protocol 8-9 is data length
pkt[10] = dLen & 255;
pkt.set(colors.subarray(cOff, cOff + dLen), 11);
if(i + cnt >= len) {
pkt[1] = 0x41; //if this is last packet, set the "push" flag to render the frame
}
try {
ws.send(pkt.buffer);
} catch (e) {
console.error(e);
return false;
}
}
return true;
}

File diff suppressed because it is too large Load Diff

View File

@@ -794,7 +794,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);

View File

@@ -672,7 +672,6 @@ function parseInfo(i) {
//syncTglRecv = i.str;
maxSeg = i.leds.maxseg;
pmt = i.fs.pmt;
if (pcMode && !i.wifi.ap) gId('edit').classList.remove("hide"); else gId('edit').classList.add("hide");
gId('buttonNodes').style.display = lastinfo.ndc > 0 ? null:"none";
// do we have a matrix set-up
mw = i.leds.matrix ? i.leds.matrix.w : 0;
@@ -694,6 +693,8 @@ function parseInfo(i) {
// gId("filterVol").classList.add("hide"); hideModes(" ♪"); // hide volume reactive effects
// gId("filterFreq").classList.add("hide"); hideModes(" ♫"); // hide frequency reactive effects
// }
// Check for version upgrades on page load
checkVersionUpgrade(i);
}
//https://stackoverflow.com/questions/2592092/executing-script-elements-inserted-with-innerhtml
@@ -3151,7 +3152,6 @@ function togglePcMode(fromB = false)
if (!fromB && ((wW < 1024 && lastw < 1024) || (wW >= 1024 && lastw >= 1024))) return; // no change in size and called from size()
if (pcMode) openTab(0, true);
gId('buttonPcm').className = (pcMode) ? "active":"";
if (pcMode && !ap) gId('edit').classList.remove("hide"); else gId('edit').classList.add("hide");
gId('bot').style.height = (pcMode && !cfg.comp.pcmbot) ? "0":"auto";
sCol('--bh', gId('bot').clientHeight + "px");
_C.style.width = (pcMode || simplifiedUI)?'100%':'400%';
@@ -3306,6 +3306,195 @@ function simplifyUI() {
gId("btns").style.display = "none";
}
// Version reporting feature
var versionCheckDone = false;
function checkVersionUpgrade(info) {
// Only check once per page load
if (versionCheckDone) return;
versionCheckDone = true;
// Suppress feature if in AP mode (no internet connection available)
if (info.wifi && info.wifi.ap) return;
// Fetch version-info.json using existing /edit endpoint
fetch(getURL('/edit?func=edit&path=/version-info.json'), {
method: 'get'
})
.then(res => {
if (res.status === 404) {
// File doesn't exist - first install, show install prompt
showVersionUpgradePrompt(info, null, info.ver);
return null;
}
if (!res.ok) {
throw new Error('Failed to fetch version-info.json');
}
return res.json();
})
.then(versionInfo => {
if (!versionInfo) return; // 404 case already handled
// Check if user opted out
if (versionInfo.neverAsk) return;
// Check if version has changed
const currentVersion = info.ver;
const storedVersion = versionInfo.version || '';
if (storedVersion && storedVersion !== currentVersion) {
// Version has changed, show upgrade prompt
showVersionUpgradePrompt(info, storedVersion, currentVersion);
} else if (!storedVersion) {
// Empty version in file, show install prompt
showVersionUpgradePrompt(info, null, currentVersion);
}
})
.catch(e => {
console.log('Failed to load version-info.json', e);
// On error, save current version for next time
if (info && info.ver) {
updateVersionInfo(info.ver, false);
}
});
}
function showVersionUpgradePrompt(info, oldVersion, newVersion) {
// Determine if this is an install or upgrade
const isInstall = !oldVersion;
// Create overlay and dialog
const overlay = d.createElement('div');
overlay.id = 'versionUpgradeOverlay';
overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.7);z-index:10000;display:flex;align-items:center;justify-content:center;';
const dialog = d.createElement('div');
dialog.style.cssText = 'background:var(--c-1);border-radius:10px;padding:25px;max-width:500px;margin:20px;box-shadow:0 4px 6px rgba(0,0,0,0.3);';
// Build contextual message based on install vs upgrade
const title = isInstall
? '🎉 Thank you for installing WLED!'
: '🎉 WLED Upgrade Detected!';
const description = isInstall
? `You are now running WLED <strong style="text-wrap: nowrap">${newVersion}</strong>.`
: `Your WLED has been upgraded from <strong style="text-wrap: nowrap">${oldVersion}</strong> to <strong style="text-wrap: nowrap">${newVersion}</strong>.`;
const question = 'Help make WLED better with a one-time hardware report? It includes only device details like chip type, LED count, etc. — never personal data or your activities.'
dialog.innerHTML = `
<h2 style="margin-top:0;color:var(--c-f);">${title}</h2>
<p style="color:var(--c-f);">${description}</p>
<p style="color:var(--c-f);">${question}</p>
<p style="color:var(--c-f);font-size:0.9em;">
<a href="https://kno.wled.ge/about/privacy-policy/" target="_blank" style="color:var(--c-6);">Learn more about what data is collected and why</a>
</p>
<div style="margin-top:20px;">
<button id="versionReportYes" class="btn">Yes</button>
<button id="versionReportNo" class="btn">Not Now</button>
<button id="versionReportNever" class="btn">Never Ask</button>
</div>
`;
overlay.appendChild(dialog);
d.body.appendChild(overlay);
// Add event listeners
gId('versionReportYes').addEventListener('click', () => {
reportUpgradeEvent(info, oldVersion);
d.body.removeChild(overlay);
});
gId('versionReportNo').addEventListener('click', () => {
// Don't update version, will ask again on next load
d.body.removeChild(overlay);
});
gId('versionReportNever').addEventListener('click', () => {
updateVersionInfo(newVersion, true);
d.body.removeChild(overlay);
showToast('You will not be asked again.');
});
}
function reportUpgradeEvent(info, oldVersion) {
showToast('Reporting upgrade...');
// Fetch fresh data from /json/info endpoint as requested
fetch(getURL('/json/info'), {
method: 'get'
})
.then(res => res.json())
.then(infoData => {
// Map to UpgradeEventRequest structure per OpenAPI spec
// Required fields: deviceId, version, previousVersion, releaseName, chip, ledCount, isMatrix, bootloaderSHA256
const upgradeData = {
deviceId: infoData.deviceId, // Use anonymous unique device ID
version: infoData.ver || '', // Current version string
previousVersion: oldVersion || '', // Previous version from version-info.json
releaseName: infoData.release || '', // Release name (e.g., "WLED 0.15.0")
chip: infoData.arch || '', // Chip architecture (esp32, esp8266, etc)
ledCount: infoData.leds ? infoData.leds.count : 0, // Number of LEDs
isMatrix: !!(infoData.leds && infoData.leds.matrix), // Whether it's a 2D matrix setup
bootloaderSHA256: infoData.bootloaderSHA256 || '', // Bootloader SHA256 hash
brand: infoData.brand, // Device brand (always present)
product: infoData.product, // Product name (always present)
flashSize: infoData.flash // Flash size (always present)
};
// Add optional fields if available
if (infoData.psram !== undefined) upgradeData.psramSize = Math.round(infoData.psram / (1024 * 1024)); // convert bytes to MB
// Note: partitionSizes not currently available in /json/info endpoint
// Make AJAX call to postUpgradeEvent API
return fetch('https://usage.wled.me/api/usage/upgrade', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(upgradeData)
});
})
.then(res => {
if (res.ok) {
showToast('Thank you for reporting!');
updateVersionInfo(info.ver, false);
} else {
showToast('Report failed. Please try again later.', true);
// Do NOT update version info on failure - user will be prompted again
}
})
.catch(e => {
console.log('Failed to report upgrade', e);
showToast('Report failed. Please try again later.', true);
// Do NOT update version info on error - user will be prompted again
});
}
function updateVersionInfo(version, neverAsk) {
const versionInfo = {
version: version,
neverAsk: neverAsk
};
// Create a Blob with JSON content and use /upload endpoint
const blob = new Blob([JSON.stringify(versionInfo)], {type: 'application/json'});
const formData = new FormData();
formData.append('data', blob, 'version-info.json');
fetch(getURL('/upload'), {
method: 'POST',
body: formData
})
.then(res => res.text())
.then(data => {
console.log('Version info updated', data);
})
.catch(e => {
console.log('Failed to update version-info.json', e);
});
}
size();
_C.style.setProperty('--n', N);

View File

@@ -17,8 +17,8 @@
position: absolute;
}
</style>
<script src="common.js"></script>
<script>
var d = document;
var ws;
var tmout = null;
var c;
@@ -62,32 +62,14 @@
if (window.location.href.indexOf("?ws") == -1) {update(); return;}
// Initialize WebSocket connection
try {
ws = top.window.ws;
} catch (e) {}
if (ws && ws.readyState === WebSocket.OPEN) {
//console.info("Peek uses top WS");
ws.send("{'lv':true}");
} else {
//console.info("Peek WS opening");
let l = window.location;
let pathn = l.pathname;
let paths = pathn.slice(1,pathn.endsWith('/')?-1:undefined).split("/");
let url = l.origin.replace("http","ws");
if (paths.length > 1) {
url += "/" + paths[0];
}
ws = new WebSocket(url+"/ws");
ws.onopen = function () {
//console.info("Peek WS open");
ws.send("{'lv':true}");
}
}
ws.binaryType = "arraybuffer";
ws = connectWs(function () {
//console.info("Peek WS open");
ws.send('{"lv":true}');
});
ws.addEventListener('message', (e) => {
try {
if (toString.call(e.data) === '[object ArrayBuffer]') {
let leds = new Uint8Array(event.data);
let leds = new Uint8Array(e.data);
if (leds[0] != 76) return; //'L'
// leds[1] = 1: 1D; leds[1] = 2: 1D/2D (leds[2]=w, leds[3]=h)
draw(leds[1]==2 ? 4 : 2, 3, leds, (a,i) => `rgb(${a[i]},${a[i+1]},${a[i+2]})`);
@@ -102,4 +84,4 @@
<body onload="S()">
<canvas id="canv"></canvas>
</body>
</html>
</html>

View File

@@ -10,6 +10,7 @@
margin: 0;
}
</style>
<script src="common.js"></script>
</head>
<body>
<canvas id="canv"></canvas>
@@ -26,30 +27,13 @@
var ctx = c.getContext('2d');
if (ctx) { // Access the rendering context
// use parent WS or open new
var ws;
try {
ws = top.window.ws;
} catch (e) {}
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send("{'lv':true}");
} else {
let l = window.location;
let pathn = l.pathname;
let paths = pathn.slice(1,pathn.endsWith('/')?-1:undefined).split("/");
let url = l.origin.replace("http","ws");
if (paths.length > 1) {
url += "/" + paths[0];
}
ws = new WebSocket(url+"/ws");
ws.onopen = ()=>{
ws.send("{'lv':true}");
}
}
ws.binaryType = "arraybuffer";
var ws = connectWs(()=>{
ws.send('{"lv":true}');
});
ws.addEventListener('message',(e)=>{
try {
if (toString.call(e.data) === '[object ArrayBuffer]') {
let leds = new Uint8Array(event.data);
let leds = new Uint8Array(e.data);
if (leds[0] != 76 || leds[1] != 2 || !ctx) return; //'L', set in ws.cpp
let mW = leds[2]; // matrix width
let mH = leds[3]; // matrix height

View File

@@ -6,7 +6,7 @@
<title>LED Settings</title>
<script src="common.js" type="text/javascript"></script>
<script>
var maxB=1,maxD=1,maxA=1,maxV=0,maxM=4000,maxPB=2048,maxL=1664,maxCO=5; //maximum bytes for LED allocation: 4kB for 8266, 32kB for 32
var maxB=1,maxD=1,maxA=1,maxV=0,maxM=4000,maxPB=2048,maxL=1664,maxCO=5,maxBT=4; //maximum bytes for LED allocation: 4kB for 8266, 32kB for 32
var customStarts=false,startsDirty=[];
function off(n) { gN(n).value = -1;}
// these functions correspond to C macros found in const.h
@@ -43,7 +43,7 @@
}); // If we set async false, file is loaded and executed, then next statement is processed
if (loc) d.Sf.action = getURL('/settings/leds');
}
function bLimits(b,v,p,m,l,o=5,d=2,a=6) {
function bLimits(b,v,p,m,l,o=5,d=2,a=6,n=4) {
maxB = b; // maxB - max physical (analog + digital) buses: 32 - ESP32, 14 - S3/S2, 6 - C3, 4 - 8266
maxV = v; // maxV - min virtual buses: 6 - ESP32/S3, 4 - S2/C3, 3 - ESP8266 (only used to distinguish S2/S3)
maxPB = p; // maxPB - max LEDs per bus
@@ -52,6 +52,7 @@
maxCO = o; // maxCO - max Color Order mappings
maxD = d; // maxD - max digital channels (can be changed if using ESP32 parallel I2S): 16 - ESP32, 12 - S3/S2, 2 - C3, 3 - 8266
maxA = a; // maxA - max analog channels: 16 - ESP32, 8 - S3/S2, 6 - C3, 5 - 8266
maxBT = n; // maxBT - max buttons
}
function is8266() { return maxA == 5 && maxD == 3; } // NOTE: see const.h
function is32() { return maxA == 16 && maxD == 16; } // NOTE: see const.h
@@ -600,9 +601,9 @@ Swap: <select id="xw${s}" name="XW${s}">
}
function addBtn(i,p,t) {
var c = gId("btns").innerHTML;
var b = gId("btns");
var s = chrID(i);
c += `Button ${i} GPIO: <input type="number" name="BT${s}" onchange="UI()" class="xs" value="${p}">`;
var c = `<div id="btn${i}">#${i} GPIO: <input type="number" name="BT${s}" onchange="UI()" min="-1" max="${d.max_gpio}" class="xs" value="${p}">`;
c += `&nbsp;<select name="BE${s}">`
c += `<option value="0" ${t==0?"selected":""}>Disabled</option>`;
c += `<option value="2" ${t==2?"selected":""}>Pushbutton</option>`;
@@ -614,8 +615,24 @@ Swap: <select id="xw${s}" name="XW${s}">
c += `<option value="8" ${t==8?"selected":""}>Analog inverted</option>`;
c += `<option value="9" ${t==9?"selected":""}>Touch (switch)</option>`;
c += `</select>`;
c += `<span style="cursor: pointer;" onclick="off('BT${s}')">&nbsp;&#x2715;</span><br>`;
gId("btns").innerHTML = c;
c += `<span style="cursor: pointer;" onclick="off('BT${s}')">&nbsp;&#x2715;</span><br></div>`;
b.insertAdjacentHTML("beforeend", c);
btnBtn();
pinDropdowns();
UI();
}
function remBtn() {
var b = gId("btns");
if (b.children.length <= 1) return;
b.lastElementChild.remove();
btnBtn();
pinDropdowns();
UI();
}
function btnBtn() {
var b = gId("btns");
gId("btn_rem").style.display = (b.children.length > 1) ? "inline" : "none";
gId("btn_add").style.display = (b.children.length < maxBT) ? "inline" : "none";
}
function tglSi(cs) {
customStarts = cs;
@@ -867,10 +884,16 @@ Swap: <select id="xw${s}" name="XW${s}">
<div id="com_entries"></div>
<hr class="sml">
<button type="button" id="com_add" onclick="addCOM()">+</button>
<button type="button" id="com_rem" onclick="remCOM()">-</button><br>
<button type="button" id="com_rem" onclick="remCOM()">-</button>
</div>
<hr class="sml">
<div id="btns"></div>
<div id="btn_wrap">
Buttons:
<div id="btns"></div>
<hr class="sml">
<button type="button" id="btn_add" onclick="addBtn(gId('btns').children.length,-1,0)">+</button>
<button type="button" id="btn_rem" onclick="remBtn()">-</button>
</div>
Disable internal pull-up/down: <input type="checkbox" name="IP"><br>
Touch threshold: <input type="number" class="s" min="0" max="100" name="TT" required><br>
<hr class="sml">

View File

@@ -17,26 +17,65 @@
}
window.open(getURL("/update?revert"),"_self");
}
function GetV() {/*injected values here*/}
function GetV() {
// Fetch device info via JSON API instead of compiling it in
fetch('/json/info')
.then(response => response.json())
.then(data => {
document.querySelector('.installed-version').textContent = `${data.brand} ${data.ver} (${data.vid})`;
document.querySelector('.release-name').textContent = data.release;
// TODO - assemble update URL
// TODO - can this be done at build time?
if (data.arch == "esp8266") {
toggle('rev');
}
const isESP32 = data.arch && (data.arch.toLowerCase() === 'esp32' || data.arch.toLowerCase() === 'esp32-s2');
if (isESP32) {
gId('bootloader-section').style.display = 'block';
if (data.bootloaderSHA256) {
gId('bootloader-hash').innerText = 'Current bootloader SHA256: ' + data.bootloaderSHA256;
}
}
})
.catch(error => {
console.log('Could not fetch device info:', error);
// Fallback to compiled-in value if API call fails
document.querySelector('.installed-version').textContent = 'Unknown';
document.querySelector('.release-name').textContent = 'Unknown';
});
}
</script>
<style>
@import url("style.css");
</style>
</head>
<body onload="GetV()">
<body onload="GetV();">
<h2>WLED Software Update</h2>
<form method='POST' action='./update' id='upd' enctype='multipart/form-data' onsubmit="toggle('upd')">
Installed version: <span class="sip">WLED ##VERSION##</span><br>
Installed version: <span class="sip installed-version">Loading...</span><br>
Release: <span class="sip release-name">Loading...</span><br>
Download the latest binary: <a href="https://github.com/wled-dev/WLED/releases" target="_blank"
style="vertical-align: text-bottom; display: inline-flex;">
<img src="https://img.shields.io/github/release/wled-dev/WLED.svg?style=flat-square"></a><br>
<input type="hidden" name="skipValidation" value="" id="sV">
<input type='file' name='update' required><br> <!--should have accept='.bin', but it prevents file upload from android app-->
<input type='checkbox' onchange="sV.value=checked?1:''" id="skipValidation">
<label for='skipValidation'>Ignore firmware validation</label><br>
<button type="submit">Update!</button><br>
<hr class="sml">
<button id="rev" type="button" onclick="cR()">Revert update</button><br>
<button type="button" onclick="B()">Back</button>
</form>
<div id="bootloader-section" style="display:none;">
<hr class="sml">
<h2>ESP32 Bootloader Update</h2>
<div id="bootloader-hash" class="sip" style="margin-bottom:8px;"></div>
<form method='POST' action='./updatebootloader' id='bootupd' enctype='multipart/form-data' onsubmit="toggle('bootupd')">
<b>Warning:</b> Only upload verified ESP32 bootloader files!<br>
<input type='file' name='update' required><br>
<button type="submit">Update Bootloader</button>
</form>
</div>
<div id="Noupd" class="hide"><b>Updating...</b><br>Please do not close or refresh the page :)</div>
</body>
</html>

View File

@@ -55,8 +55,8 @@ static dmx_config_t createConfig()
config.software_version_id = VERSION;
strcpy(config.device_label, "WLED_MM");
const std::string versionString = "WLED_V" + std::to_string(VERSION);
strncpy(config.software_version_label, versionString.c_str(), 32);
const std::string dmxWledVersionString = "WLED_V" + std::to_string(VERSION);
strncpy(config.software_version_label, dmxWledVersionString.c_str(), 32);
config.software_version_label[32] = '\0'; // zero termination in case versionString string was longer than 32 chars
config.personalities[0].description = "SINGLE_RGB";

View File

@@ -30,11 +30,19 @@ void handleDDPPacket(e131_packet_t* p) {
uint32_t start = htonl(p->channelOffset) / ddpChannelsPerLed;
start += DMXAddress / ddpChannelsPerLed;
unsigned stop = start + htons(p->dataLen) / ddpChannelsPerLed;
uint16_t dataLen = htons(p->dataLen);
unsigned stop = start + dataLen / ddpChannelsPerLed;
uint8_t* data = p->data;
unsigned c = 0;
if (p->flags & DDP_TIMECODE_FLAG) c = 4; //packet has timecode flag, we do not support it, but data starts 4 bytes later
unsigned numLeds = stop - start; // stop >= start is guaranteed
unsigned maxDataIndex = c + numLeds * ddpChannelsPerLed; // validate bounds before accessing data array
if (maxDataIndex > dataLen) {
DEBUG_PRINTLN(F("DDP packet data bounds exceeded, rejecting."));
return;
}
if (realtimeMode != REALTIME_MODE_DDP) ddpSeenPush = false; // just starting, no push yet
realtimeLock(realtimeTimeoutMs, REALTIME_MODE_DDP);
@@ -414,7 +422,7 @@ void prepareArtnetPollReply(ArtPollReply *reply) {
reply->reply_port = ARTNET_DEFAULT_PORT;
char * numberEnd = versionString;
char * numberEnd = (char*) versionString; // strtol promises not to try to edit this.
reply->reply_version_h = (uint8_t)strtol(numberEnd, &numberEnd, 10);
numberEnd++;
reply->reply_version_l = (uint8_t)strtol(numberEnd, &numberEnd, 10);

View File

@@ -27,6 +27,7 @@ void IRAM_ATTR touchButtonISR();
bool backupConfig();
bool restoreConfig();
bool verifyConfig();
bool configBackupExists();
void resetConfig();
bool deserializeConfig(JsonObject doc, bool fromFS = false);
bool deserializeConfigFromFS();
@@ -103,6 +104,7 @@ inline bool readObjectFromFile(const String &file, const char* key, JsonDocument
bool copyFile(const char* src_path, const char* dst_path);
bool backupFile(const char* filename);
bool restoreFile(const char* filename);
bool checkBackupExists(const char* filename);
bool validateJsonFile(const char* filename);
void dumpFilesToSerial();
@@ -399,6 +401,8 @@ uint8_t extractModeSlider(uint8_t mode, uint8_t slider, char *dest, uint8_t maxL
int16_t extractModeDefaults(uint8_t mode, const char *segVar);
void checkSettingsPIN(const char *pin);
uint16_t crc16(const unsigned char* data_p, size_t length);
String computeSHA1(const String& input);
String getDeviceId();
uint16_t beatsin88_t(accum88 beats_per_minute_88, uint16_t lowest = 0, uint16_t highest = 65535, uint32_t timebase = 0, uint16_t phase_offset = 0);
uint16_t beatsin16_t(accum88 beats_per_minute, uint16_t lowest = 0, uint16_t highest = 65535, uint32_t timebase = 0, uint16_t phase_offset = 0);
uint8_t beatsin8_t(accum88 beats_per_minute, uint8_t lowest = 0, uint8_t highest = 255, uint32_t timebase = 0, uint8_t phase_offset = 0);
@@ -539,7 +543,6 @@ void handleSerial();
void updateBaudRate(uint32_t rate);
//wled_server.cpp
void createEditHandler(bool enable);
void initServer();
void serveMessage(AsyncWebServerRequest* request, uint16_t code, const String& headl, const String& subl="", byte optionT=255);
void serveJsonError(AsyncWebServerRequest* request, uint16_t code, uint16_t error);

View File

@@ -557,6 +557,12 @@ bool restoreFile(const char* filename) {
return false;
}
bool checkBackupExists(const char* filename) {
char backupname[32];
snprintf_P(backupname, sizeof(backupname), s_backup_fmt, filename + 1); // skip leading '/' in filename
return WLED_FS.exists(backupname);
}
bool validateJsonFile(const char* filename) {
if (!WLED_FS.exists(filename)) return false;
File file = WLED_FS.open(filename, "r");

View File

@@ -9,11 +9,11 @@
* Functions to render images from filesystem to segments, used by the "Image" effect
*/
File file;
char lastFilename[34] = "/";
GifDecoder<320,320,12,true> decoder;
bool gifDecodeFailed = false;
unsigned long lastFrameDisplayTime = 0, currentFrameDelay = 0;
static File file;
static char lastFilename[WLED_MAX_SEGNAME_LEN+2] = "/"; // enough space for "/" + seg.name + '\0'
static GifDecoder<320,320,12,true> decoder; // this creates the basic object; parameter lzwMaxBits is not used; decoder.alloc() always allocated "everything else" = 24Kb
static bool gifDecodeFailed = false;
static unsigned long lastFrameDisplayTime = 0, currentFrameDelay = 0;
bool fileSeekCallback(unsigned long position) {
return file.seek(position);
@@ -35,29 +35,62 @@ int fileSizeCallback(void) {
return file.size();
}
bool openGif(const char *filename) {
bool openGif(const char *filename) { // side-effect: updates "file"
file = WLED_FS.open(filename, "r");
DEBUG_PRINTF_P(PSTR("opening GIF file %s\n"), filename);
if (!file) return false;
return true;
}
Segment* activeSeg;
uint16_t gifWidth, gifHeight;
static Segment* activeSeg;
static uint16_t gifWidth, gifHeight;
static int lastCoordinate; // last coordinate (x+y) that was set, used to reduce redundant pixel writes
static uint16_t perPixelX, perPixelY; // scaling factors when upscaling
void screenClearCallback(void) {
activeSeg->fill(0);
}
void updateScreenCallback(void) {}
// this callback runs when the decoder has finished painting all pixels
void updateScreenCallback(void) {
// perfect time for adding blur
if (activeSeg->intensity > 1) {
uint8_t blurAmount = activeSeg->intensity;
if ((blurAmount < 24) && (activeSeg->is2D())) activeSeg->blurRows(activeSeg->intensity); // some blur - fast
else activeSeg->blur(blurAmount); // more blur - slower
}
lastCoordinate = -1; // invalidate last position
}
void drawPixelCallback(int16_t x, int16_t y, uint8_t red, uint8_t green, uint8_t blue) {
// simple nearest-neighbor scaling
int16_t outY = y * activeSeg->height() / gifHeight;
int16_t outX = x * activeSeg->width() / gifWidth;
// note: GifDecoder drawing is done top right to bottom left, line by line
// callbacks to draw a pixel at (x,y) without scaling: used if GIF size matches (virtual)segment size (faster) works for 1D and 2D segments
void drawPixelCallbackNoScale(int16_t x, int16_t y, uint8_t red, uint8_t green, uint8_t blue) {
activeSeg->setPixelColor(y * gifWidth + x, red, green, blue);
}
void drawPixelCallback1D(int16_t x, int16_t y, uint8_t red, uint8_t green, uint8_t blue) {
// 1D strip: load pixel-by-pixel left to right, top to bottom (0/0 = top-left in gifs)
int totalImgPix = (int)gifWidth * gifHeight;
int start = ((int)y * gifWidth + (int)x) * activeSeg->vLength() / totalImgPix; // simple nearest-neighbor scaling
if (start == lastCoordinate) return; // skip setting same coordinate again
lastCoordinate = start;
for (int i = 0; i < perPixelX; i++) {
activeSeg->setPixelColor(start + i, red, green, blue);
}
}
void drawPixelCallback2D(int16_t x, int16_t y, uint8_t red, uint8_t green, uint8_t blue) {
// simple nearest-neighbor scaling
int outY = (int)y * activeSeg->vHeight() / gifHeight;
int outX = (int)x * activeSeg->vWidth() / gifWidth;
// Pack coordinates uniquely: outY into upper 16 bits, outX into lower 16 bits
if (((outY << 16) | outX) == lastCoordinate) return; // skip setting same coordinate again
lastCoordinate = (outY << 16) | outX; // since input is a "scanline" this is sufficient to identify a "unique" coordinate
// set multiple pixels if upscaling
for (int16_t i = 0; i < (activeSeg->width()+(gifWidth-1)) / gifWidth; i++) {
for (int16_t j = 0; j < (activeSeg->height()+(gifHeight-1)) / gifHeight; j++) {
for (int i = 0; i < perPixelX; i++) {
for (int j = 0; j < perPixelY; j++) {
activeSeg->setPixelColorXY(outX + i, outY + j, red, green, blue);
}
}
@@ -79,31 +112,88 @@ byte renderImageToSegment(Segment &seg) {
if (!seg.name) return IMAGE_ERROR_NO_NAME;
// disable during effect transition, causes flickering, multiple allocations and depending on image, part of old FX remaining
//if (seg.mode != seg.currentMode()) return IMAGE_ERROR_WAITING;
if (activeSeg && activeSeg != &seg) return IMAGE_ERROR_SEG_LIMIT; // only one segment at a time
if (activeSeg && activeSeg != &seg) { // only one segment at a time
if (!seg.isActive()) return IMAGE_ERROR_SEG_LIMIT; // sanity check: calling segment must be active
if (gifDecodeFailed || !activeSeg->isActive()) // decoder failed, or last segment became inactive
endImagePlayback(activeSeg); // => allow takeover but clean up first
else
return IMAGE_ERROR_SEG_LIMIT;
}
activeSeg = &seg;
if (strncmp(lastFilename +1, seg.name, 32) != 0) { // segment name changed, load new image
strncpy(lastFilename +1, seg.name, 32);
if (strncmp(lastFilename +1, seg.name, WLED_MAX_SEGNAME_LEN) != 0) { // segment name changed, load new image
strcpy(lastFilename, "/"); // filename always starts with '/'
strncpy(lastFilename +1, seg.name, WLED_MAX_SEGNAME_LEN);
lastFilename[WLED_MAX_SEGNAME_LEN+1] ='\0'; // ensure proper string termination when segment name was truncated
gifDecodeFailed = false;
if (strcmp(lastFilename + strlen(lastFilename) - 4, ".gif") != 0) {
size_t fnameLen = strlen(lastFilename);
if ((fnameLen < 4) || strcmp(lastFilename + fnameLen - 4, ".gif") != 0) { // empty segment name, name too short, or name not ending in .gif
gifDecodeFailed = true;
DEBUG_PRINTF_P(PSTR("GIF decoder unsupported file: %s\n"), lastFilename);
return IMAGE_ERROR_UNSUPPORTED_FORMAT;
}
if (file) file.close();
openGif(lastFilename);
if (!file) { gifDecodeFailed = true; return IMAGE_ERROR_FILE_MISSING; }
if (!openGif(lastFilename)) {
gifDecodeFailed = true;
DEBUG_PRINTF_P(PSTR("GIF file not found: %s\n"), lastFilename);
return IMAGE_ERROR_FILE_MISSING;
}
lastCoordinate = -1;
decoder.setScreenClearCallback(screenClearCallback);
decoder.setUpdateScreenCallback(updateScreenCallback);
decoder.setDrawPixelCallback(drawPixelCallback);
decoder.setDrawPixelCallback(drawPixelCallbackNoScale); // default: use "fast path" callback without scaling
decoder.setFileSeekCallback(fileSeekCallback);
decoder.setFilePositionCallback(filePositionCallback);
decoder.setFileReadCallback(fileReadCallback);
decoder.setFileReadBlockCallback(fileReadBlockCallback);
decoder.setFileSizeCallback(fileSizeCallback);
decoder.alloc();
#if __cpp_exceptions // use exception handler if we can (some targets don't support exceptions)
try {
#endif
decoder.alloc(); // this function may throw out-of memory and cause a crash
#if __cpp_exceptions
} catch (...) { // if we arrive here, the decoder has thrown an OOM exception
gifDecodeFailed = true;
errorFlag = ERR_NORAM_PX;
DEBUG_PRINTLN(F("\nGIF decoder out of memory. Please try a smaller image file.\n"));
return IMAGE_ERROR_DECODER_ALLOC;
// decoder cleanup (hi @coderabbitai): No additonal cleanup necessary - decoder.alloc() ultimately uses "new AnimatedGIF".
// If new throws, no pointer is assigned, previous decoder state (if any) has already been deleted inside alloc(), so calling decoder.dealloc() here is unnecessary.
}
#endif
DEBUG_PRINTLN(F("Starting decoding"));
if(decoder.startDecoding() < 0) { gifDecodeFailed = true; return IMAGE_ERROR_GIF_DECODE; }
int decoderError = decoder.startDecoding();
if(decoderError < 0) {
DEBUG_PRINTF_P(PSTR("GIF Decoding error %d in startDecoding().\n"), decoderError);
errorFlag = ERR_NORAM_PX;
gifDecodeFailed = true;
return IMAGE_ERROR_GIF_DECODE;
}
DEBUG_PRINTLN(F("Decoding started"));
// after startDecoding, we can get GIF size, update static variables and callbacks
decoder.getSize(&gifWidth, &gifHeight);
if (gifWidth == 0 || gifHeight == 0) { // bad gif size: prevent division by zero
gifDecodeFailed = true;
DEBUG_PRINTF_P(PSTR("Invalid GIF dimensions: %dx%d\n"), gifWidth, gifHeight);
return IMAGE_ERROR_GIF_DECODE;
}
if (activeSeg->is2D()) {
perPixelX = (activeSeg->vWidth() + gifWidth -1) / gifWidth;
perPixelY = (activeSeg->vHeight() + gifHeight-1) / gifHeight;
if (activeSeg->vWidth() != gifWidth || activeSeg->vHeight() != gifHeight) {
decoder.setDrawPixelCallback(drawPixelCallback2D); // use 2D callback with scaling
//DEBUG_PRINTLN(F("scaling image"));
}
} else {
int totalImgPix = (int)gifWidth * gifHeight;
if (totalImgPix - activeSeg->vLength() == 1) totalImgPix--; // handle off-by-one: skip last pixel instead of first (gifs constructed from 1D input pad last pixel if length is odd)
perPixelX = (activeSeg->vLength() + totalImgPix-1) / totalImgPix;
if (totalImgPix != activeSeg->vLength()) {
decoder.setDrawPixelCallback(drawPixelCallback1D); // use 1D callback with scaling
//DEBUG_PRINTLN(F("scaling image"));
}
}
}
if (gifDecodeFailed) return IMAGE_ERROR_PREV;
@@ -117,10 +207,12 @@ byte renderImageToSegment(Segment &seg) {
// TODO consider handling this on FX level with a different frametime, but that would cause slow gifs to speed up during transitions
if (millis() - lastFrameDisplayTime < wait) return IMAGE_ERROR_WAITING;
decoder.getSize(&gifWidth, &gifHeight);
int result = decoder.decodeFrame(false);
if (result < 0) { gifDecodeFailed = true; return IMAGE_ERROR_FRAME_DECODE; }
if (result < 0) {
DEBUG_PRINTF_P(PSTR("GIF Decoding error %d in decodeFrame().\n"), result);
gifDecodeFailed = true;
return IMAGE_ERROR_FRAME_DECODE;
}
currentFrameDelay = decoder.getFrameDelay_ms();
unsigned long tooSlowBy = (millis() - lastFrameDisplayTime) - wait; // if last frame was longer than intended, compensate
@@ -137,7 +229,8 @@ void endImagePlayback(Segment *seg) {
decoder.dealloc();
gifDecodeFailed = false;
activeSeg = nullptr;
lastFilename[1] = '\0';
strcpy(lastFilename, "/"); // reset filename
gifWidth = gifHeight = 0; // reset dimensions
DEBUG_PRINTLN(F("Image playback ended"));
}

View File

@@ -1,5 +1,6 @@
#include "wled.h"
#define JSON_PATH_STATE 1
#define JSON_PATH_INFO 2
#define JSON_PATH_STATE_INFO 3
@@ -51,6 +52,9 @@ namespace {
if (a.custom1 != b.custom1) d |= SEG_DIFFERS_FX;
if (a.custom2 != b.custom2) d |= SEG_DIFFERS_FX;
if (a.custom3 != b.custom3) d |= SEG_DIFFERS_FX;
if (a.check1 != b.check1) d |= SEG_DIFFERS_FX;
if (a.check2 != b.check2) d |= SEG_DIFFERS_FX;
if (a.check3 != b.check3) d |= SEG_DIFFERS_FX;
if (a.startY != b.startY) d |= SEG_DIFFERS_BOUNDS;
if (a.stopY != b.stopY) d |= SEG_DIFFERS_BOUNDS;
@@ -64,22 +68,6 @@ namespace {
}
}
/**
* Deserialize a segment description from a JSON object and apply it to the specified segment slot.
*
* Parses and applies geometry, naming, grouping/spacing/offset, 2D bounds, mode, palette, colors
* (supports kelvin, hex, "r" random, object or array formats), per-LED assignments, options (on/frz/sel/rev/mi),
* speed/intensity, custom channels, checks, blend mode, and LOXONE mappings. The function may append a new
* segment, delete a segment, perform a repeat expansion to create multiple segments, and mark global state
* as changed when segment parameters differ.
*
* @param elem JSON object describing the segment (API format).
* @param it Default segment index to use when `elem["id"]` is not provided.
* @param presetId Optional preset identifier; when nonzero, preset-related side effects (e.g., playlist unloading)
* are suppressed or handled differently.
* @return true if the JSON was valid for the target id and the segment was applied (or created); false if the
* target id is out of range or the descriptor is invalid (e.g., attempting to create an empty segment).
*/
static bool deserializeSegment(JsonObject elem, byte it, byte presetId = 0)
{
byte id = elem["id"] | it;
@@ -221,7 +209,7 @@ static bool deserializeSegment(JsonObject elem, byte it, byte presetId = 0)
// JSON "col" array can contain the following values for each of segment's colors (primary, background, custom):
// "col":[int|string|object|array, int|string|object|array, int|string|object|array]
// int = Kelvin temperature or 0 for black
// string = hex representation of [WW]RRGGBB or "r" for random color
// string = hex representation of [WW]RRGGBB
// object = individual channel control {"r":0,"g":127,"b":255,"w":255}, each being optional (valid to send {})
// array = direct channel values [r,g,b,w] (w element being optional)
int rgbw[] = {0,0,0,0};
@@ -245,9 +233,6 @@ static bool deserializeSegment(JsonObject elem, byte it, byte presetId = 0)
if (kelvin == 0) seg.setColor(i, 0);
if (kelvin > 0) colorKtoRGB(kelvin, brgbw);
colValid = true;
} else if (hexCol[0] == 'r' && hexCol[1] == '\0') { // Random colors via JSON API in Segment object like col=["r","r","r"] · Issue #4996
setRandomColor(brgbw);
colValid = true;
} else { //HEX string, e.g. "FFAA00"
colValid = colorFromHexString(brgbw, hexCol);
}
@@ -706,6 +691,7 @@ void serializeState(JsonObject root, bool forPreset, bool includeBri, bool segme
}
}
void serializeInfo(JsonObject root)
{
root[F("ver")] = versionString;
@@ -713,6 +699,7 @@ void serializeInfo(JsonObject root)
root[F("cn")] = F(WLED_CODENAME);
root[F("release")] = releaseString;
root[F("repo")] = repoString;
root[F("deviceId")] = getDeviceId();
JsonObject leds = root.createNestedObject(F("leds"));
leds[F("count")] = strip.getLengthTotal();
@@ -836,6 +823,9 @@ void serializeInfo(JsonObject root)
root[F("resetReason1")] = (int)rtc_get_reset_reason(1);
#endif
root[F("lwip")] = 0; //deprecated
#ifndef WLED_DISABLE_OTA
root[F("bootloaderSHA256")] = getBootloaderSHA256Hex();
#endif
#else
root[F("arch")] = "esp8266";
root[F("core")] = ESP.getCoreVersion();
@@ -1259,4 +1249,4 @@ bool serveLiveLeds(AsyncWebServerRequest* request, uint32_t wsClient)
#endif
return true;
}
#endif
#endif

741
wled00/ota_update.cpp Normal file
View File

@@ -0,0 +1,741 @@
#include "ota_update.h"
#include "wled.h"
#ifdef ESP32
#include <esp_app_format.h>
#include <esp_ota_ops.h>
#include <esp_flash.h>
#include <mbedtls/sha256.h>
#endif
// Platform-specific metadata locations
#ifdef ESP32
constexpr size_t METADATA_OFFSET = 256; // ESP32: metadata appears after Espressif metadata
#define UPDATE_ERROR errorString
const size_t BOOTLOADER_OFFSET = 0x1000;
#elif defined(ESP8266)
constexpr size_t METADATA_OFFSET = 0x1000; // ESP8266: metadata appears at 4KB offset
#define UPDATE_ERROR getErrorString
#endif
constexpr size_t METADATA_SEARCH_RANGE = 512; // bytes
/**
* Check if OTA should be allowed based on release compatibility using custom description
* @param binaryData Pointer to binary file data (not modified)
* @param dataSize Size of binary data in bytes
* @param errorMessage Buffer to store error message if validation fails
* @param errorMessageLen Maximum length of error message buffer
* @return true if OTA should proceed, false if it should be blocked
*/
static bool validateOTA(const uint8_t* binaryData, size_t dataSize, char* errorMessage, size_t errorMessageLen) {
// Clear error message
if (errorMessage && errorMessageLen > 0) {
errorMessage[0] = '\0';
}
// Try to extract WLED structure directly from binary data
wled_metadata_t extractedDesc;
bool hasDesc = findWledMetadata(binaryData, dataSize, &extractedDesc);
if (hasDesc) {
return shouldAllowOTA(extractedDesc, errorMessage, errorMessageLen);
} else {
// No custom description - this could be a legacy binary
if (errorMessage && errorMessageLen > 0) {
strncpy_P(errorMessage, PSTR("This firmware file is missing compatibility metadata."), errorMessageLen - 1);
errorMessage[errorMessageLen - 1] = '\0';
}
return false;
}
}
struct UpdateContext {
// State flags
// FUTURE: the flags could be replaced by a state machine
bool replySent = false;
bool needsRestart = false;
bool updateStarted = false;
bool uploadComplete = false;
bool releaseCheckPassed = false;
String errorMessage;
// Buffer to hold block data across posts, if needed
std::vector<uint8_t> releaseMetadataBuffer;
};
static void endOTA(AsyncWebServerRequest *request) {
UpdateContext* context = reinterpret_cast<UpdateContext*>(request->_tempObject);
request->_tempObject = nullptr;
DEBUG_PRINTF_P(PSTR("EndOTA %x --> %x (%d)\n"), (uintptr_t)request,(uintptr_t) context, context ? context->uploadComplete : 0);
if (context) {
if (context->updateStarted) { // We initialized the update
// We use Update.end() because not all forms of Update() support an abort.
// If the upload is incomplete, Update.end(false) should error out.
if (Update.end(context->uploadComplete)) {
// Update successful!
#ifndef ESP8266
bootloopCheckOTA(); // let the bootloop-checker know there was an OTA update
#endif
doReboot = true;
context->needsRestart = false;
}
}
if (context->needsRestart) {
strip.resume();
UsermodManager::onUpdateBegin(false);
#if WLED_WATCHDOG_TIMEOUT > 0
WLED::instance().enableWatchdog();
#endif
}
delete context;
}
};
static bool beginOTA(AsyncWebServerRequest *request, UpdateContext* context)
{
#ifdef ESP8266
Update.runAsync(true);
#endif
if (Update.isRunning()) {
request->send(503);
setOTAReplied(request);
return false;
}
#if WLED_WATCHDOG_TIMEOUT > 0
WLED::instance().disableWatchdog();
#endif
UsermodManager::onUpdateBegin(true); // notify usermods that update is about to begin (some may require task de-init)
strip.suspend();
backupConfig(); // backup current config in case the update ends badly
strip.resetSegments(); // free as much memory as you can
context->needsRestart = true;
DEBUG_PRINTF_P(PSTR("OTA Update Start, %x --> %x\n"), (uintptr_t)request,(uintptr_t) context);
auto skipValidationParam = request->getParam("skipValidation", true);
if (skipValidationParam && (skipValidationParam->value() == "1")) {
context->releaseCheckPassed = true;
DEBUG_PRINTLN(F("OTA validation skipped by user"));
}
// Begin update with the firmware size from content length
size_t updateSize = request->contentLength() > 0 ? request->contentLength() : ((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000);
if (!Update.begin(updateSize)) {
context->errorMessage = Update.UPDATE_ERROR();
DEBUG_PRINTF_P(PSTR("OTA Failed to begin: %s\n"), context->errorMessage.c_str());
return false;
}
context->updateStarted = true;
return true;
}
// Create an OTA context object on an AsyncWebServerRequest
// Returns true if successful, false on failure.
bool initOTA(AsyncWebServerRequest *request) {
// Allocate update context
UpdateContext* context = new (std::nothrow) UpdateContext {};
if (context) {
request->_tempObject = context;
request->onDisconnect([=]() { endOTA(request); }); // ensures we restart on failure
};
DEBUG_PRINTF_P(PSTR("OTA Update init, %x --> %x\n"), (uintptr_t)request,(uintptr_t) context);
return (context != nullptr);
}
void setOTAReplied(AsyncWebServerRequest *request) {
UpdateContext* context = reinterpret_cast<UpdateContext*>(request->_tempObject);
if (!context) return;
context->replySent = true;
};
// Returns pointer to error message, or nullptr if OTA was successful.
std::pair<bool, String> getOTAResult(AsyncWebServerRequest* request) {
UpdateContext* context = reinterpret_cast<UpdateContext*>(request->_tempObject);
if (!context) return { true, F("OTA context unexpectedly missing") };
if (context->replySent) return { false, {} };
if (context->errorMessage.length()) return { true, context->errorMessage };
if (context->updateStarted) {
// Release the OTA context now.
endOTA(request);
if (Update.hasError()) {
return { true, Update.UPDATE_ERROR() };
} else {
return { true, {} };
}
}
// Should never happen
return { true, F("Internal software failure") };
}
void handleOTAData(AsyncWebServerRequest *request, size_t index, uint8_t *data, size_t len, bool isFinal)
{
UpdateContext* context = reinterpret_cast<UpdateContext*>(request->_tempObject);
if (!context) return;
//DEBUG_PRINTF_P(PSTR("HandleOTAData: %d %d %d\n"), index, len, isFinal);
if (context->replySent || (context->errorMessage.length())) return;
if (index == 0) {
if (!beginOTA(request, context)) return;
}
// Perform validation if we haven't done it yet and we have reached the metadata offset
if (!context->releaseCheckPassed && (index+len) > METADATA_OFFSET) {
// Current chunk contains the metadata offset
size_t availableDataAfterOffset = (index + len) - METADATA_OFFSET;
DEBUG_PRINTF_P(PSTR("OTA metadata check: %d in buffer, %d received, %d available\n"), context->releaseMetadataBuffer.size(), len, availableDataAfterOffset);
if (availableDataAfterOffset >= METADATA_SEARCH_RANGE) {
// We have enough data to validate, one way or another
const uint8_t* search_data = data;
size_t search_len = len;
// If we have saved data, use that instead
if (context->releaseMetadataBuffer.size()) {
// Add this data
context->releaseMetadataBuffer.insert(context->releaseMetadataBuffer.end(), data, data+len);
search_data = context->releaseMetadataBuffer.data();
search_len = context->releaseMetadataBuffer.size();
}
// Do the checking
char errorMessage[128];
bool OTA_ok = validateOTA(search_data, search_len, errorMessage, sizeof(errorMessage));
// Release buffer if there was one
context->releaseMetadataBuffer = decltype(context->releaseMetadataBuffer){};
if (!OTA_ok) {
DEBUG_PRINTF_P(PSTR("OTA declined: %s\n"), errorMessage);
context->errorMessage = errorMessage;
context->errorMessage += F(" Enable 'Ignore firmware validation' to proceed anyway.");
return;
} else {
DEBUG_PRINTLN(F("OTA allowed: Release compatibility check passed"));
context->releaseCheckPassed = true;
}
} else {
// Store the data we just got for next pass
context->releaseMetadataBuffer.insert(context->releaseMetadataBuffer.end(), data, data+len);
}
}
// Check if validation was still pending (shouldn't happen normally)
// This is done before writing the last chunk, so endOTA can abort
if (isFinal && !context->releaseCheckPassed) {
DEBUG_PRINTLN(F("OTA failed: Validation never completed"));
// Don't write the last chunk to the updater: this will trip an error later
context->errorMessage = F("Release check data never arrived?");
return;
}
// Write chunk data to OTA update (only if release check passed or still pending)
if (!Update.hasError()) {
if (Update.write(data, len) != len) {
DEBUG_PRINTF_P(PSTR("OTA write failed on chunk %zu: %s\n"), index, Update.UPDATE_ERROR());
}
}
if(isFinal) {
DEBUG_PRINTLN(F("OTA Update End"));
// Upload complete
context->uploadComplete = true;
}
}
void markOTAvalid() {
#ifndef ESP8266
const esp_partition_t* running = esp_ota_get_running_partition();
esp_ota_img_states_t ota_state;
if (esp_ota_get_state_partition(running, &ota_state) == ESP_OK) {
if (ota_state == ESP_OTA_IMG_PENDING_VERIFY) {
esp_ota_mark_app_valid_cancel_rollback(); // only needs to be called once, it marks the ota_state as ESP_OTA_IMG_VALID
DEBUG_PRINTLN(F("Current firmware validated"));
}
}
#endif
}
#if defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DISABLE_OTA)
// Cache for bootloader SHA256 digest as hex string
static String bootloaderSHA256HexCache = "";
// Calculate and cache the bootloader SHA256 digest as hex string
void calculateBootloaderSHA256() {
if (!bootloaderSHA256HexCache.isEmpty()) return;
// Bootloader is at fixed offset 0x1000 (4KB) and is typically 32KB
const uint32_t bootloaderSize = 0x8000; // 32KB, typical bootloader size
// Calculate SHA256
uint8_t sha256[32];
mbedtls_sha256_context ctx;
mbedtls_sha256_init(&ctx);
mbedtls_sha256_starts(&ctx, 0); // 0 = SHA256 (not SHA224)
const size_t chunkSize = 256;
uint8_t buffer[chunkSize];
for (uint32_t offset = 0; offset < bootloaderSize; offset += chunkSize) {
size_t readSize = min((size_t)(bootloaderSize - offset), chunkSize);
if (esp_flash_read(NULL, buffer, BOOTLOADER_OFFSET + offset, readSize) == ESP_OK) {
mbedtls_sha256_update(&ctx, buffer, readSize);
}
}
mbedtls_sha256_finish(&ctx, sha256);
mbedtls_sha256_free(&ctx);
// Convert to hex string and cache it
char hex[65];
for (int i = 0; i < 32; i++) {
sprintf(hex + (i * 2), "%02x", sha256[i]);
}
hex[64] = '\0';
bootloaderSHA256HexCache = String(hex);
}
// Get bootloader SHA256 as hex string
String getBootloaderSHA256Hex() {
calculateBootloaderSHA256();
return bootloaderSHA256HexCache;
}
// Invalidate cached bootloader SHA256 (call after bootloader update)
void invalidateBootloaderSHA256Cache() {
bootloaderSHA256HexCache = "";
}
// Verify complete buffered bootloader using ESP-IDF validation approach
// This matches the key validation steps from esp_image_verify() in ESP-IDF
// Returns the actual bootloader data pointer and length via the buffer and len parameters
bool verifyBootloaderImage(const uint8_t* &buffer, size_t &len, String* bootloaderErrorMsg) {
size_t availableLen = len;
if (!bootloaderErrorMsg) {
DEBUG_PRINTLN(F("bootloaderErrorMsg is null"));
return false;
}
// ESP32 image header structure (based on esp_image_format.h)
// Offset 0: magic (0xE9)
// Offset 1: segment_count
// Offset 2: spi_mode
// Offset 3: spi_speed (4 bits) + spi_size (4 bits)
// Offset 4-7: entry_addr (uint32_t)
// Offset 8: wp_pin
// Offset 9-11: spi_pin_drv[3]
// Offset 12-13: chip_id (uint16_t, little-endian)
// Offset 14: min_chip_rev
// Offset 15-22: reserved[8]
// Offset 23: hash_appended
const size_t MIN_IMAGE_HEADER_SIZE = 24;
// 1. Validate minimum size for header
if (len < MIN_IMAGE_HEADER_SIZE) {
*bootloaderErrorMsg = "Bootloader too small - invalid header";
return false;
}
// Check if the bootloader starts at offset 0x1000 (common in partition table dumps)
// This happens when someone uploads a complete flash dump instead of just the bootloader
if (len > BOOTLOADER_OFFSET + MIN_IMAGE_HEADER_SIZE &&
buffer[BOOTLOADER_OFFSET] == 0xE9 &&
buffer[0] != 0xE9) {
DEBUG_PRINTF_P(PSTR("Bootloader magic byte detected at offset 0x%04X - adjusting buffer\n"), BOOTLOADER_OFFSET);
// Adjust buffer pointer to start at the actual bootloader
buffer = buffer + BOOTLOADER_OFFSET;
len = len - BOOTLOADER_OFFSET;
// Re-validate size after adjustment
if (len < MIN_IMAGE_HEADER_SIZE) {
*bootloaderErrorMsg = "Bootloader at offset 0x1000 too small - invalid header";
return false;
}
}
// 2. Magic byte check (matches esp_image_verify step 1)
if (buffer[0] != 0xE9) {
*bootloaderErrorMsg = "Invalid bootloader magic byte (expected 0xE9, got 0x" + String(buffer[0], HEX) + ")";
return false;
}
// 3. Segment count validation (matches esp_image_verify step 2)
uint8_t segmentCount = buffer[1];
if (segmentCount == 0 || segmentCount > 16) {
*bootloaderErrorMsg = "Invalid segment count: " + String(segmentCount);
return false;
}
// 4. SPI mode validation (basic sanity check)
uint8_t spiMode = buffer[2];
if (spiMode > 3) { // Valid modes are 0-3 (QIO, QOUT, DIO, DOUT)
*bootloaderErrorMsg = "Invalid SPI mode: " + String(spiMode);
return false;
}
// 5. Chip ID validation (matches esp_image_verify step 3)
uint16_t chipId = buffer[12] | (buffer[13] << 8); // Little-endian
// Known ESP32 chip IDs from ESP-IDF:
// 0x0000 = ESP32
// 0x0002 = ESP32-S2
// 0x0005 = ESP32-C3
// 0x0009 = ESP32-S3
// 0x000C = ESP32-C2
// 0x000D = ESP32-C6
// 0x0010 = ESP32-H2
#if defined(CONFIG_IDF_TARGET_ESP32)
if (chipId != 0x0000) {
*bootloaderErrorMsg = "Chip ID mismatch - expected ESP32 (0x0000), got 0x" + String(chipId, HEX);
return false;
}
#elif defined(CONFIG_IDF_TARGET_ESP32S2)
if (chipId != 0x0002) {
*bootloaderErrorMsg = "Chip ID mismatch - expected ESP32-S2 (0x0002), got 0x" + String(chipId, HEX);
return false;
}
#elif defined(CONFIG_IDF_TARGET_ESP32C3)
if (chipId != 0x0005) {
*bootloaderErrorMsg = "Chip ID mismatch - expected ESP32-C3 (0x0005), got 0x" + String(chipId, HEX);
return false;
}
*bootloaderErrorMsg = "ESP32-C3 update not supported yet";
return false;
#elif defined(CONFIG_IDF_TARGET_ESP32S3)
if (chipId != 0x0009) {
*bootloaderErrorMsg = "Chip ID mismatch - expected ESP32-S3 (0x0009), got 0x" + String(chipId, HEX);
return false;
}
*bootloaderErrorMsg = "ESP32-S3 update not supported yet";
return false;
#elif defined(CONFIG_IDF_TARGET_ESP32C6)
if (chipId != 0x000D) {
*bootloaderErrorMsg = "Chip ID mismatch - expected ESP32-C6 (0x000D), got 0x" + String(chipId, HEX);
return false;
}
*bootloaderErrorMsg = "ESP32-C6 update not supported yet";
return false;
#else
// Generic validation - chip ID should be valid
if (chipId > 0x00FF) {
*bootloaderErrorMsg = "Invalid chip ID: 0x" + String(chipId, HEX);
return false;
}
*bootloaderErrorMsg = "Unknown ESP32 target - bootloader update not supported";
return false;
#endif
// 6. Entry point validation (should be in valid memory range)
uint32_t entryAddr = buffer[4] | (buffer[5] << 8) | (buffer[6] << 16) | (buffer[7] << 24);
// ESP32 bootloader entry points are typically in IRAM range (0x40000000 - 0x40400000)
// or ROM range (0x40000000 and above)
if (entryAddr < 0x40000000 || entryAddr > 0x50000000) {
*bootloaderErrorMsg = "Invalid entry address: 0x" + String(entryAddr, HEX);
return false;
}
// 7. Basic segment structure validation
// Each segment has a header: load_addr (4 bytes) + data_len (4 bytes)
size_t offset = MIN_IMAGE_HEADER_SIZE;
size_t actualBootloaderSize = MIN_IMAGE_HEADER_SIZE;
for (uint8_t i = 0; i < segmentCount && offset + 8 <= len; i++) {
uint32_t segmentSize = buffer[offset + 4] | (buffer[offset + 5] << 8) |
(buffer[offset + 6] << 16) | (buffer[offset + 7] << 24);
// Segment size sanity check
// ESP32 classic bootloader segments can be larger, C3 are smaller
if (segmentSize > 0x20000) { // 128KB max per segment (very generous)
*bootloaderErrorMsg = "Segment " + String(i) + " too large: " + String(segmentSize) + " bytes";
return false;
}
offset += 8 + segmentSize; // Skip segment header and data
}
actualBootloaderSize = offset;
// 8. Check for appended SHA256 hash (byte 23 in header)
// If hash_appended != 0, there's a 32-byte SHA256 hash after the segments
uint8_t hashAppended = buffer[23];
if (hashAppended != 0) {
actualBootloaderSize += 32;
if (actualBootloaderSize > availableLen) {
*bootloaderErrorMsg = "Bootloader missing SHA256 trailer";
return false;
}
DEBUG_PRINTF_P(PSTR("Bootloader has appended SHA256 hash\n"));
}
// 9. The image may also have a 1-byte checksum after segments/hash
// Check if there's at least one more byte available
if (actualBootloaderSize + 1 <= availableLen) {
// There's likely a checksum byte
actualBootloaderSize += 1;
} else if (actualBootloaderSize > availableLen) {
*bootloaderErrorMsg = "Bootloader truncated before checksum";
return false;
}
// 10. Align to 16 bytes (ESP32 requirement for flash writes)
// The bootloader image must be 16-byte aligned
if (actualBootloaderSize % 16 != 0) {
size_t alignedSize = ((actualBootloaderSize + 15) / 16) * 16;
// Make sure we don't exceed available data
if (alignedSize <= len) {
actualBootloaderSize = alignedSize;
}
}
DEBUG_PRINTF_P(PSTR("Bootloader validation: %d segments, actual size %d bytes (buffer size %d bytes, hash_appended=%d)\n"),
segmentCount, actualBootloaderSize, len, hashAppended);
// 11. Verify we have enough data for all segments + hash + checksum
if (actualBootloaderSize > availableLen) {
*bootloaderErrorMsg = "Bootloader truncated - expected at least " + String(actualBootloaderSize) + " bytes, have " + String(availableLen) + " bytes";
return false;
}
if (offset > availableLen) {
*bootloaderErrorMsg = "Bootloader truncated - expected at least " + String(offset) + " bytes, have " + String(len) + " bytes";
return false;
}
// Update len to reflect actual bootloader size (including hash and checksum, with alignment)
// This is critical - we must write the complete image including checksums
len = actualBootloaderSize;
return true;
}
// Bootloader OTA context structure
struct BootloaderUpdateContext {
// State flags
bool replySent = false;
bool uploadComplete = false;
String errorMessage;
// Buffer to hold bootloader data
uint8_t* buffer = nullptr;
size_t bytesBuffered = 0;
const uint32_t bootloaderOffset = 0x1000;
const uint32_t maxBootloaderSize = 0x10000; // 64KB buffer size
};
// Cleanup bootloader OTA context
static void endBootloaderOTA(AsyncWebServerRequest *request) {
BootloaderUpdateContext* context = reinterpret_cast<BootloaderUpdateContext*>(request->_tempObject);
request->_tempObject = nullptr;
DEBUG_PRINTF_P(PSTR("EndBootloaderOTA %x --> %x\n"), (uintptr_t)request, (uintptr_t)context);
if (context) {
if (context->buffer) {
free(context->buffer);
context->buffer = nullptr;
}
// If update failed, restore system state
if (!context->uploadComplete || !context->errorMessage.isEmpty()) {
strip.resume();
#if WLED_WATCHDOG_TIMEOUT > 0
WLED::instance().enableWatchdog();
#endif
}
delete context;
}
}
// Initialize bootloader OTA context
bool initBootloaderOTA(AsyncWebServerRequest *request) {
if (request->_tempObject) {
return true; // Already initialized
}
BootloaderUpdateContext* context = new BootloaderUpdateContext();
if (!context) {
DEBUG_PRINTLN(F("Failed to allocate bootloader OTA context"));
return false;
}
request->_tempObject = context;
request->onDisconnect([=]() { endBootloaderOTA(request); }); // ensures cleanup on disconnect
DEBUG_PRINTLN(F("Bootloader Update Start - initializing buffer"));
#if WLED_WATCHDOG_TIMEOUT > 0
WLED::instance().disableWatchdog();
#endif
lastEditTime = millis(); // make sure PIN does not lock during update
strip.suspend();
strip.resetSegments();
// Check available heap before attempting allocation
size_t freeHeap = getFreeHeapSize();
DEBUG_PRINTF_P(PSTR("Free heap before bootloader buffer allocation: %d bytes (need %d bytes)\n"), freeHeap, context->maxBootloaderSize);
context->buffer = (uint8_t*)malloc(context->maxBootloaderSize);
if (!context->buffer) {
size_t freeHeapNow = getFreeHeapSize();
DEBUG_PRINTF_P(PSTR("Failed to allocate %d byte bootloader buffer! Free heap: %d bytes\n"), context->maxBootloaderSize, freeHeapNow);
context->errorMessage = "Out of memory! Free heap: " + String(freeHeapNow) + " bytes, need: " + String(context->maxBootloaderSize) + " bytes";
strip.resume();
#if WLED_WATCHDOG_TIMEOUT > 0
WLED::instance().enableWatchdog();
#endif
return false;
}
context->bytesBuffered = 0;
return true;
}
// Set bootloader OTA replied flag
void setBootloaderOTAReplied(AsyncWebServerRequest *request) {
BootloaderUpdateContext* context = reinterpret_cast<BootloaderUpdateContext*>(request->_tempObject);
if (context) {
context->replySent = true;
}
}
// Get bootloader OTA result
std::pair<bool, String> getBootloaderOTAResult(AsyncWebServerRequest *request) {
BootloaderUpdateContext* context = reinterpret_cast<BootloaderUpdateContext*>(request->_tempObject);
if (!context) {
return std::make_pair(true, String(F("Internal error: No bootloader OTA context")));
}
bool needsReply = !context->replySent;
String errorMsg = context->errorMessage;
// If upload was successful, return empty string and trigger reboot
if (context->uploadComplete && errorMsg.isEmpty()) {
doReboot = true;
endBootloaderOTA(request);
return std::make_pair(needsReply, String());
}
// If there was an error, return it
if (!errorMsg.isEmpty()) {
endBootloaderOTA(request);
return std::make_pair(needsReply, errorMsg);
}
// Should never happen
return std::make_pair(true, String(F("Internal software failure")));
}
// Handle bootloader OTA data
void handleBootloaderOTAData(AsyncWebServerRequest *request, size_t index, uint8_t *data, size_t len, bool isFinal) {
BootloaderUpdateContext* context = reinterpret_cast<BootloaderUpdateContext*>(request->_tempObject);
if (!context) {
DEBUG_PRINTLN(F("No bootloader OTA context - ignoring data"));
return;
}
if (!context->errorMessage.isEmpty()) {
return;
}
// Buffer the incoming data
if (context->buffer && context->bytesBuffered + len <= context->maxBootloaderSize) {
memcpy(context->buffer + context->bytesBuffered, data, len);
context->bytesBuffered += len;
DEBUG_PRINTF_P(PSTR("Bootloader buffer progress: %d / %d bytes\n"), context->bytesBuffered, context->maxBootloaderSize);
} else if (!context->buffer) {
DEBUG_PRINTLN(F("Bootloader buffer not allocated!"));
context->errorMessage = "Internal error: Bootloader buffer not allocated";
return;
} else {
size_t totalSize = context->bytesBuffered + len;
DEBUG_PRINTLN(F("Bootloader size exceeds maximum!"));
context->errorMessage = "Bootloader file too large: " + String(totalSize) + " bytes (max: " + String(context->maxBootloaderSize) + " bytes)";
return;
}
// Only write to flash when upload is complete
if (isFinal) {
DEBUG_PRINTLN(F("Bootloader Upload Complete - validating and flashing"));
if (context->buffer && context->bytesBuffered > 0) {
// Prepare pointers for verification (may be adjusted if bootloader at offset)
const uint8_t* bootloaderData = context->buffer;
size_t bootloaderSize = context->bytesBuffered;
// Verify the complete bootloader image before flashing
// Note: verifyBootloaderImage may adjust bootloaderData pointer and bootloaderSize
// for validation purposes only
if (!verifyBootloaderImage(bootloaderData, bootloaderSize, &context->errorMessage)) {
DEBUG_PRINTLN(F("Bootloader validation failed!"));
// Error message already set by verifyBootloaderImage
} else {
// Calculate offset to write to flash
// If bootloaderData was adjusted (partition table detected), we need to skip it in flash too
size_t flashOffset = context->bootloaderOffset;
const uint8_t* dataToWrite = context->buffer;
size_t bytesToWrite = context->bytesBuffered;
// If validation adjusted the pointer, it means we have a partition table at the start
// In this case, we should skip writing the partition table and write bootloader at 0x1000
if (bootloaderData != context->buffer) {
// bootloaderData was adjusted - skip partition table in our data
size_t partitionTableSize = bootloaderData - context->buffer;
dataToWrite = bootloaderData;
bytesToWrite = bootloaderSize;
DEBUG_PRINTF_P(PSTR("Skipping %d bytes of partition table data\n"), partitionTableSize);
}
DEBUG_PRINTF_P(PSTR("Bootloader validation passed - writing %d bytes to flash at 0x%04X\n"),
bytesToWrite, flashOffset);
// Calculate erase size (must be multiple of 4KB)
size_t eraseSize = ((bytesToWrite + 0xFFF) / 0x1000) * 0x1000;
if (eraseSize > context->maxBootloaderSize) {
eraseSize = context->maxBootloaderSize;
}
// Erase bootloader region
DEBUG_PRINTF_P(PSTR("Erasing %d bytes at 0x%04X...\n"), eraseSize, flashOffset);
esp_err_t err = esp_flash_erase_region(NULL, flashOffset, eraseSize);
if (err != ESP_OK) {
DEBUG_PRINTF_P(PSTR("Bootloader erase error: %d\n"), err);
context->errorMessage = "Flash erase failed (error code: " + String(err) + ")";
} else {
// Write the validated bootloader data to flash
err = esp_flash_write(NULL, dataToWrite, flashOffset, bytesToWrite);
if (err != ESP_OK) {
DEBUG_PRINTF_P(PSTR("Bootloader flash write error: %d\n"), err);
context->errorMessage = "Flash write failed (error code: " + String(err) + ")";
} else {
DEBUG_PRINTF_P(PSTR("Bootloader Update Success - %d bytes written to 0x%04X\n"),
bytesToWrite, flashOffset);
// Invalidate cached bootloader hash
invalidateBootloaderSHA256Cache();
context->uploadComplete = true;
}
}
}
} else if (context->bytesBuffered == 0) {
context->errorMessage = "No bootloader data received";
}
}
}
#endif

120
wled00/ota_update.h Normal file
View File

@@ -0,0 +1,120 @@
// WLED OTA update interface
#include <Arduino.h>
#ifdef ESP8266
#include <Updater.h>
#else
#include <Update.h>
#endif
#pragma once
// Platform-specific metadata locations
#ifdef ESP32
#define BUILD_METADATA_SECTION ".rodata_custom_desc"
#elif defined(ESP8266)
#define BUILD_METADATA_SECTION ".ver_number"
#endif
class AsyncWebServerRequest;
/**
* Create an OTA context object on an AsyncWebServerRequest
* @param request Pointer to web request object
* @return true if allocation was successful, false if not
*/
bool initOTA(AsyncWebServerRequest *request);
/**
* Indicate to the OTA subsystem that a reply has already been generated
* @param request Pointer to web request object
*/
void setOTAReplied(AsyncWebServerRequest *request);
/**
* Retrieve the OTA result.
* @param request Pointer to web request object
* @return bool indicating if a reply is necessary; string with error message if the update failed.
*/
std::pair<bool, String> getOTAResult(AsyncWebServerRequest *request);
/**
* Process a block of OTA data. This is a passthrough of an ArUploadHandlerFunction.
* Requires that initOTA be called on the handler object before any work will be done.
* @param request Pointer to web request object
* @param index Offset in to uploaded file
* @param data New data bytes
* @param len Length of new data bytes
* @param isFinal Indicates that this is the last block
* @return bool indicating if a reply is necessary; string with error message if the update failed.
*/
void handleOTAData(AsyncWebServerRequest *request, size_t index, uint8_t *data, size_t len, bool isFinal);
/**
* Mark currently running firmware as valid to prevent auto-rollback on reboot.
* This option can be enabled in some builds/bootloaders, it is an sdkconfig flag.
*/
void markOTAvalid();
#if defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DISABLE_OTA)
/**
* Calculate and cache the bootloader SHA256 digest
* Reads the bootloader from flash at offset 0x1000 and computes SHA256 hash
*/
void calculateBootloaderSHA256();
/**
* Get bootloader SHA256 as hex string
* @return String containing 64-character hex representation of SHA256 hash
*/
String getBootloaderSHA256Hex();
/**
* Invalidate cached bootloader SHA256 (call after bootloader update)
* Forces recalculation on next call to calculateBootloaderSHA256 or getBootloaderSHA256Hex
*/
void invalidateBootloaderSHA256Cache();
/**
* Verify complete buffered bootloader using ESP-IDF validation approach
* This matches the key validation steps from esp_image_verify() in ESP-IDF
* @param buffer Reference to pointer to bootloader binary data (will be adjusted if offset detected)
* @param len Reference to length of bootloader data (will be adjusted to actual size)
* @param bootloaderErrorMsg Pointer to String to store error message (must not be null)
* @return true if validation passed, false otherwise
*/
bool verifyBootloaderImage(const uint8_t* &buffer, size_t &len, String* bootloaderErrorMsg);
/**
* Create a bootloader OTA context object on an AsyncWebServerRequest
* @param request Pointer to web request object
* @return true if allocation was successful, false if not
*/
bool initBootloaderOTA(AsyncWebServerRequest *request);
/**
* Indicate to the bootloader OTA subsystem that a reply has already been generated
* @param request Pointer to web request object
*/
void setBootloaderOTAReplied(AsyncWebServerRequest *request);
/**
* Retrieve the bootloader OTA result.
* @param request Pointer to web request object
* @return bool indicating if a reply is necessary; string with error message if the update failed.
*/
std::pair<bool, String> getBootloaderOTAResult(AsyncWebServerRequest *request);
/**
* Process a block of bootloader OTA data. This is a passthrough of an ArUploadHandlerFunction.
* Requires that initBootloaderOTA be called on the handler object before any work will be done.
* @param request Pointer to web request object
* @param index Offset in to uploaded file
* @param data New data bytes
* @param len Length of new data bytes
* @param isFinal Indicates that this is the last block
*/
void handleBootloaderOTAData(AsyncWebServerRequest *request, size_t index, uint8_t *data, size_t len, bool isFinal);
#endif

View File

@@ -128,12 +128,12 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage)
PinManager::deallocatePin(irPin, PinOwner::IR);
}
#endif
for (unsigned s=0; s<WLED_MAX_BUTTONS; s++) {
if (btnPin[s]>=0 && PinManager::isPinAllocated(btnPin[s], PinOwner::Button)) {
PinManager::deallocatePin(btnPin[s], PinOwner::Button);
for (const auto &button : buttons) {
if (button.pin >= 0 && PinManager::isPinAllocated(button.pin, PinOwner::Button)) {
PinManager::deallocatePin(button.pin, PinOwner::Button);
#ifdef SOC_TOUCH_VERSION_2 // ESP32 S2 and S3 have a function to check touch state, detach interrupt
if (digitalPinToTouchChannel(btnPin[s]) >= 0) // if touch capable pin
touchDetachInterrupt(btnPin[s]); // if not assigned previously, this will do nothing
if (digitalPinToTouchChannel(button.pin) >= 0) // if touch capable pin
touchDetachInterrupt(button.pin); // if not assigned previously, this will do nothing
#endif
}
}
@@ -280,54 +280,56 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage)
char bt[4] = "BT"; bt[2] = offset+i; bt[3] = 0; // button pin (use A,B,C,... if WLED_MAX_BUTTONS>10)
char be[4] = "BE"; be[2] = offset+i; be[3] = 0; // button type (use A,B,C,... if WLED_MAX_BUTTONS>10)
int hw_btn_pin = request->arg(bt).toInt();
if (hw_btn_pin >= 0 && PinManager::allocatePin(hw_btn_pin,false,PinOwner::Button)) {
btnPin[i] = hw_btn_pin;
buttonType[i] = request->arg(be).toInt();
#ifdef ARDUINO_ARCH_ESP32
if (i >= buttons.size()) buttons.emplace_back(hw_btn_pin, request->arg(be).toInt()); // add button to vector
else {
buttons[i].pin = hw_btn_pin;
buttons[i].type = request->arg(be).toInt();
}
if (buttons[i].pin >= 0 && PinManager::allocatePin(buttons[i].pin, false, PinOwner::Button)) {
#ifdef ARDUINO_ARCH_ESP32
// ESP32 only: check that button pin is a valid gpio
if ((buttonType[i] == BTN_TYPE_ANALOG) || (buttonType[i] == BTN_TYPE_ANALOG_INVERTED))
{
if (digitalPinToAnalogChannel(btnPin[i]) < 0) {
if ((buttons[i].type == BTN_TYPE_ANALOG) || (buttons[i].type == BTN_TYPE_ANALOG_INVERTED)) {
if (digitalPinToAnalogChannel(buttons[i].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[i], i);
btnPin[i] = -1;
PinManager::deallocatePin(hw_btn_pin,PinOwner::Button);
DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for analog button #%d is not an analog pin!\n"), buttons[i].pin, i);
PinManager::deallocatePin(buttons[i].pin, PinOwner::Button);
buttons[i].type = BTN_TYPE_NONE;
} else {
analogReadResolution(12); // see #4040
}
}
else if ((buttonType[i] == BTN_TYPE_TOUCH || buttonType[i] == BTN_TYPE_TOUCH_SWITCH))
{
if (digitalPinToTouchChannel(btnPin[i]) < 0)
{
} else if ((buttons[i].type == BTN_TYPE_TOUCH || buttons[i].type == BTN_TYPE_TOUCH_SWITCH)) {
if (digitalPinToTouchChannel(buttons[i].pin) < 0) {
// not a touch pin
DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for touch button #%d is not an touch pin!\n"), btnPin[i], i);
btnPin[i] = -1;
PinManager::deallocatePin(hw_btn_pin,PinOwner::Button);
DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for touch button #%d is not an touch pin!\n"), buttons[i].pin, i);
PinManager::deallocatePin(buttons[i].pin, PinOwner::Button);
buttons[i].type = BTN_TYPE_NONE;
}
#ifdef SOC_TOUCH_VERSION_2 // ESP32 S2 and S3 have a fucntion to check touch state but need to attach an interrupt to do so
else
{
touchAttachInterrupt(btnPin[i], 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 touchAttachInterrupt(buttons[i].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
{
// regular buttons and switches
if (disablePullUp) {
pinMode(btnPin[i], INPUT);
pinMode(buttons[i].pin, INPUT);
} else {
#ifdef ESP32
pinMode(btnPin[i], buttonType[i]==BTN_TYPE_PUSH_ACT_HIGH ? INPUT_PULLDOWN : INPUT_PULLUP);
pinMode(buttons[i].pin, buttons[i].type==BTN_TYPE_PUSH_ACT_HIGH ? INPUT_PULLDOWN : INPUT_PULLUP);
#else
pinMode(btnPin[i], INPUT_PULLUP);
pinMode(buttons[i].pin, INPUT_PULLUP);
#endif
}
}
} else {
btnPin[i] = -1;
buttonType[i] = BTN_TYPE_NONE;
buttons[i].pin = -1;
buttons[i].type = BTN_TYPE_NONE;
}
}
// we should remove all unused buttons from the vector
for (int i = buttons.size()-1; i > 0; i--) {
if (buttons[i].pin < 0 && buttons[i].type == BTN_TYPE_NONE) {
buttons.erase(buttons.begin() + i); // remove button from vector
}
}
@@ -531,14 +533,16 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage)
macroAlexaOff = request->arg(F("A1")).toInt();
macroCountdown = request->arg(F("MC")).toInt();
macroNl = request->arg(F("MN")).toInt();
for (unsigned i=0; i<WLED_MAX_BUTTONS; i++) {
char mp[4] = "MP"; mp[2] = (i<10?48:55)+i; mp[3] = 0; // short
char ml[4] = "ML"; ml[2] = (i<10?48:55)+i; ml[3] = 0; // long
char md[4] = "MD"; md[2] = (i<10?48:55)+i; md[3] = 0; // double
int i = 0;
for (auto &button : buttons) {
char mp[4] = "MP"; mp[2] = (i<10?'0':'A'-10)+i; mp[3] = 0; // short
char ml[4] = "ML"; ml[2] = (i<10?'0':'A'-10)+i; ml[3] = 0; // long
char md[4] = "MD"; md[2] = (i<10?'0':'A'-10)+i; md[3] = 0; // double
//if (!request->hasArg(mp)) break;
macroButton[i] = request->arg(mp).toInt(); // these will default to 0 if not present
macroLongPress[i] = request->arg(ml).toInt();
macroDoublePress[i] = request->arg(md).toInt();
button.macroButton = request->arg(mp).toInt(); // these will default to 0 if not present
button.macroLongPress = request->arg(ml).toInt();
button.macroDoublePress = request->arg(md).toInt();
i++;
}
char k[3]; k[2] = 0;
@@ -613,7 +617,6 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage)
#ifndef WLED_DISABLE_OTA
aOtaEnabled = request->hasArg(F("AO"));
#endif
//createEditHandler(correctPIN && !otaLock);
otaSameSubnet = request->hasArg(F("SU"));
}
}
@@ -815,8 +818,13 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage)
}
}
strip.panel.shrink_to_fit(); // release unused memory
// we are changing matrix/ledmap geometry which *will* affect existing segments
// since we are not in loop() context we must make sure that effects are not running. credit @blazonchek for properly fixing #4911
strip.suspend();
strip.waitForIt();
strip.deserializeMap(); // (re)load default ledmap (will also setUpMatrix() if ledmap does not exist)
strip.makeAutoSegments(true); // force re-creation of segments
strip.resume();
}
#endif

View File

@@ -3,6 +3,7 @@
#include "const.h"
#ifdef ESP8266
#include "user_interface.h" // for bootloop detection
#include <Hash.h> // for SHA1 on ESP8266
#else
#include <Update.h>
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0)
@@ -10,6 +11,8 @@
#elif ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(3, 3, 0)
#include "soc/rtc.h"
#endif
#include "mbedtls/sha1.h" // for SHA1 on ESP32
#include "esp_efuse.h"
#endif
@@ -369,7 +372,6 @@ void checkSettingsPIN(const char* pin) {
if (!correctPIN && millis() - lastEditTime < PIN_RETRY_COOLDOWN) return; // guard against PIN brute force
bool correctBefore = correctPIN;
correctPIN = (strlen(settingsPIN) == 0 || strncmp(settingsPIN, pin, 4) == 0);
if (correctBefore != correctPIN) createEditHandler(correctPIN);
lastEditTime = millis();
}
@@ -634,10 +636,12 @@ int32_t hw_random(int32_t lowerlimit, int32_t upperlimit) {
#if defined(IDF_TARGET_ESP32C3) || defined(ESP8266)
#error "ESP32-C3 and ESP8266 with PSRAM is not supported, please remove BOARD_HAS_PSRAM definition"
#else
// BOARD_HAS_PSRAM also means that compiler flag "-mfix-esp32-psram-cache-issue" has to be used
#if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32S3) // PSRAM fix only needed for classic esp32
// BOARD_HAS_PSRAM also means that compiler flag "-mfix-esp32-psram-cache-issue" has to be used for old "rev.1" esp32
#warning "BOARD_HAS_PSRAM defined, make sure to use -mfix-esp32-psram-cache-issue to prevent issues on rev.1 ESP32 boards \
see https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-guides/external-ram.html#esp32-rev-v1-0"
#endif
#endif
#else
#if !defined(IDF_TARGET_ESP32C3) && !defined(ESP8266)
#pragma message("BOARD_HAS_PSRAM not defined, not using PSRAM.")
@@ -648,7 +652,8 @@ int32_t hw_random(int32_t lowerlimit, int32_t upperlimit) {
#ifdef ESP8266
static void *validateFreeHeap(void *buffer) {
// make sure there is enough free heap left if buffer was allocated in DRAM region, free it if not
if (getContiguousFreeHeap() < MIN_HEAP_SIZE) {
// note: ESP826 needs very little contiguous heap for webserver, checking total free heap works better
if (getFreeHeapSize() < MIN_HEAP_SIZE) {
free(buffer);
return nullptr;
}
@@ -1125,4 +1130,98 @@ uint8_t perlin8(uint16_t x, uint16_t y) {
uint8_t perlin8(uint16_t x, uint16_t y, uint16_t z) {
return (((perlin3D_raw((uint32_t)x << 8, (uint32_t)y << 8, (uint32_t)z << 8, true) * 2015) >> 10) + 33168) >> 8; //scale to 16 bit, offset, then scale to 8bit
}
}
// Platform-agnostic SHA1 computation from String input
String computeSHA1(const String& input) {
#ifdef ESP8266
return sha1(input); // ESP8266 has built-in sha1() function
#else
// ESP32: Compute SHA1 hash using mbedtls
unsigned char shaResult[20]; // SHA1 produces 20 bytes
mbedtls_sha1_context ctx;
mbedtls_sha1_init(&ctx);
mbedtls_sha1_starts_ret(&ctx);
mbedtls_sha1_update_ret(&ctx, (const unsigned char*)input.c_str(), input.length());
mbedtls_sha1_finish_ret(&ctx, shaResult);
mbedtls_sha1_free(&ctx);
// Convert to hexadecimal string
char hexString[41];
for (int i = 0; i < 20; i++) {
sprintf(&hexString[i*2], "%02x", shaResult[i]);
}
hexString[40] = '\0';
return String(hexString);
#endif
}
#ifdef ESP32
#include "esp_adc_cal.h"
String generateDeviceFingerprint() {
uint32_t fp[2] = {0, 0}; // create 64 bit fingerprint
esp_chip_info_t chip_info;
esp_chip_info(&chip_info);
esp_efuse_mac_get_default((uint8_t*)fp);
fp[1] ^= ESP.getFlashChipSize();
fp[0] ^= chip_info.full_revision | (chip_info.model << 16);
// mix in ADC calibration data:
esp_adc_cal_characteristics_t ch;
#if SOC_ADC_MAX_BITWIDTH == 13 // S2 has 13 bit ADC
#define BIT_WIDTH ADC_WIDTH_BIT_13
#else
#define BIT_WIDTH ADC_WIDTH_BIT_12
#endif
esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_DB_11, BIT_WIDTH, 1100, &ch);
fp[0] ^= ch.coeff_a;
fp[1] ^= ch.coeff_b;
if (ch.low_curve) {
for (int i = 0; i < 8; i++) {
fp[0] ^= ch.low_curve[i];
}
}
if (ch.high_curve) {
for (int i = 0; i < 8; i++) {
fp[1] ^= ch.high_curve[i];
}
}
char fp_string[17]; // 16 hex chars + null terminator
sprintf(fp_string, "%08X%08X", fp[1], fp[0]);
return String(fp_string);
}
#else // ESP8266
String generateDeviceFingerprint() {
uint32_t fp[2] = {0, 0}; // create 64 bit fingerprint
WiFi.macAddress((uint8_t*)&fp); // use MAC address as fingerprint base
fp[0] ^= ESP.getFlashChipId();
fp[1] ^= ESP.getFlashChipSize() | ESP.getFlashChipVendorId() << 16;
char fp_string[17]; // 16 hex chars + null terminator
sprintf(fp_string, "%08X%08X", fp[1], fp[0]);
return String(fp_string);
}
#endif
// Generate a device ID based on SHA1 hash of MAC address salted with other unique device info
// Returns: original SHA1 + last 2 chars of double-hashed SHA1 (42 chars total)
String getDeviceId() {
static String cachedDeviceId = "";
if (cachedDeviceId.length() > 0) return cachedDeviceId;
// The device string is deterministic as it needs to be consistent for the same device, even after a full flash erase
// MAC is salted with other consistent device info to avoid rainbow table attacks.
// If the MAC address is known by malicious actors, they could precompute SHA1 hashes to impersonate devices,
// but as WLED developers are just looking at statistics and not authenticating devices, this is acceptable.
// If the usage data was exfiltrated, you could not easily determine the MAC from the device ID without brute forcing SHA1
String firstHash = computeSHA1(generateDeviceFingerprint());
// Second hash: SHA1 of the first hash
String secondHash = computeSHA1(firstHash);
// Concatenate first hash + last 2 chars of second hash
cachedDeviceId = firstHash + secondHash.substring(38);
return cachedDeviceId;
}

View File

@@ -1,6 +1,7 @@
#define WLED_DEFINE_GLOBAL_VARS //only in one source file, wled.cpp!
#include "wled.h"
#include "wled_ethernet.h"
#include "ota_update.h"
#ifdef WLED_ENABLE_AOTA
#define NO_OTA_PORT
#include <ArduinoOTA.h>
@@ -166,16 +167,15 @@ void WLED::loop()
// 15min PIN time-out
if (strlen(settingsPIN)>0 && correctPIN && millis() - lastEditTime > PIN_TIMEOUT) {
correctPIN = false;
createEditHandler(false);
}
// reconnect WiFi to clear stale allocations if heap gets too low
if (millis() - heapTime > 15000) {
uint32_t heap = getFreeHeapSize();
if (heap < MIN_HEAP_SIZE && lastHeap < MIN_HEAP_SIZE) {
DEBUG_PRINTF_P(PSTR("Heap too low! %u\n"), heap);
forceReconnect = true;
DEBUG_PRINTF_P(PSTR("Heap too low! %u\n"), heap);
strip.resetSegments(); // remove all but one segments from memory
if (!Update.isRunning()) forceReconnect = true;
} else if (heap < MIN_HEAP_SIZE) {
DEBUG_PRINTLN(F("Heap low, purging segments."));
strip.purgeSegments();
@@ -474,7 +474,7 @@ void WLED::setup()
if (needsCfgSave) serializeConfigToFS(); // usermods required new parameters; need to wait for strip to be initialised #4752
if (strcmp(multiWiFi[0].clientSSID, DEFAULT_CLIENT_SSID) == 0)
if (strcmp(multiWiFi[0].clientSSID, DEFAULT_CLIENT_SSID) == 0 && !configBackupExists())
showWelcomePage = true;
WiFi.persistent(false);
WiFi.onEvent(WiFiEvent);
@@ -555,6 +555,7 @@ void WLED::setup()
#if defined(ARDUINO_ARCH_ESP32) && defined(WLED_DISABLE_BROWNOUT_DET)
WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 1); //enable brownout detector
#endif
markOTAvalid();
}
void WLED::beginStrip()

View File

@@ -189,11 +189,15 @@ using PSRAMDynamicJsonDocument = BasicJsonDocument<PSRAM_Allocator>;
#include "FastLED.h"
#include "const.h"
#include "fcn_declare.h"
#ifndef WLED_DISABLE_OTA
#include "ota_update.h"
#endif
#include "NodeStruct.h"
#include "pin_manager.h"
#include "colors.h"
#include "bus_manager.h"
#include "FX.h"
#include "wled_metadata.h"
#ifndef CLIENT_SSID
#define CLIENT_SSID DEFAULT_CLIENT_SSID
@@ -270,20 +274,6 @@ using PSRAMDynamicJsonDocument = BasicJsonDocument<PSRAM_Allocator>;
#define STRINGIFY(X) #X
#define TOSTRING(X) STRINGIFY(X)
#ifndef WLED_VERSION
#define WLED_VERSION dev
#endif
#ifndef WLED_RELEASE_NAME
#define WLED_RELEASE_NAME "Custom"
#endif
#ifndef WLED_REPO
#define WLED_REPO "unknown"
#endif
// Global Variable definitions
WLED_GLOBAL char versionString[] _INIT(TOSTRING(WLED_VERSION));
WLED_GLOBAL char releaseString[] _INIT(WLED_RELEASE_NAME); // must include the quotes when defining, e.g -D WLED_RELEASE_NAME=\"ESP32_MULTI_USREMODS\"
WLED_GLOBAL char repoString[] _INIT(WLED_REPO);
#define WLED_CODENAME "Niji"
// AP and OTA default passwords (for maximum security change them!)
@@ -296,10 +286,10 @@ WLED_GLOBAL char otaPass[33] _INIT(DEFAULT_OTA_PASS);
// Hardware and pin config
#ifndef BTNPIN
#define BTNPIN 0,-1
#define BTNPIN 0
#endif
#ifndef BTNTYPE
#define BTNTYPE BTN_TYPE_PUSH,BTN_TYPE_NONE
#define BTNTYPE BTN_TYPE_PUSH
#endif
#ifndef RLYPIN
WLED_GLOBAL int8_t rlyPin _INIT(-1);
@@ -375,7 +365,7 @@ WLED_GLOBAL wifi_options_t wifiOpt _INIT_N(({0, 1, false, AP_BEHAVIOR_BOOT_NO_CO
#define force802_3g wifiOpt.force802_3g
#else
WLED_GLOBAL int8_t selectedWiFi _INIT(0);
WLED_GLOBAL byte apChannel _INIT(1); // 2.4GHz WiFi AP channel (1-13)
WLED_GLOBAL byte apChannel _INIT(6); // 2.4GHz WiFi AP channel (1-13)
WLED_GLOBAL byte apHide _INIT(0); // hidden AP SSID
WLED_GLOBAL byte apBehavior _INIT(AP_BEHAVIOR_BOOT_NO_CONN); // access point opens when no connection after boot by default
#ifdef ARDUINO_ARCH_ESP32
@@ -581,9 +571,6 @@ WLED_GLOBAL byte countdownMin _INIT(0) , countdownSec _INIT(0);
WLED_GLOBAL byte macroNl _INIT(0); // after nightlight delay over
WLED_GLOBAL byte macroCountdown _INIT(0);
WLED_GLOBAL byte macroAlexaOn _INIT(0), macroAlexaOff _INIT(0);
WLED_GLOBAL byte macroButton[WLED_MAX_BUTTONS] _INIT({0});
WLED_GLOBAL byte macroLongPress[WLED_MAX_BUTTONS] _INIT({0});
WLED_GLOBAL byte macroDoublePress[WLED_MAX_BUTTONS] _INIT({0});
// Security CONFIG
#ifdef WLED_OTA_PASS
@@ -649,13 +636,32 @@ WLED_GLOBAL byte briLast _INIT(128); // brightness before
WLED_GLOBAL byte whiteLast _INIT(128); // white channel before turned off. Used for toggle function in ir.cpp
// button
WLED_GLOBAL int8_t btnPin[WLED_MAX_BUTTONS] _INIT({BTNPIN});
WLED_GLOBAL byte buttonType[WLED_MAX_BUTTONS] _INIT({BTNTYPE});
struct Button {
unsigned long pressedTime; // time button was pressed
unsigned long waitTime; // time to wait for next button press
int8_t pin; // pin number
struct {
uint8_t type : 6; // button type (push, long, double, etc.)
bool pressedBefore : 1; // button was pressed before
bool longPressed : 1; // button was long pressed
};
uint8_t macroButton; // macro/preset to call on button press
uint8_t macroLongPress; // macro/preset to call on long press
uint8_t macroDoublePress; // macro/preset to call on double press
Button(int8_t p, uint8_t t, uint8_t mB = 0, uint8_t mLP = 0, uint8_t mDP = 0)
: pressedTime(0)
, waitTime(0)
, pin(p)
, type(t)
, pressedBefore(false)
, longPressed(false)
, macroButton(mB)
, macroLongPress(mLP)
, macroDoublePress(mDP) {}
};
WLED_GLOBAL std::vector<Button> buttons; // vector of button structs
WLED_GLOBAL bool buttonPublishMqtt _INIT(false);
WLED_GLOBAL bool buttonPressedBefore[WLED_MAX_BUTTONS] _INIT({false});
WLED_GLOBAL bool buttonLongPressed[WLED_MAX_BUTTONS] _INIT({false});
WLED_GLOBAL unsigned long buttonPressedTime[WLED_MAX_BUTTONS] _INIT({0});
WLED_GLOBAL unsigned long buttonWaitTime[WLED_MAX_BUTTONS] _INIT({0});
WLED_GLOBAL bool disablePullUp _INIT(false);
WLED_GLOBAL byte touchThreshold _INIT(TOUCH_THRESHOLD);

164
wled00/wled_metadata.cpp Normal file
View File

@@ -0,0 +1,164 @@
#include "ota_update.h"
#include "wled.h"
#include "wled_metadata.h"
#ifndef WLED_VERSION
#warning WLED_VERSION was not set - using default value of 'dev'
#define WLED_VERSION dev
#endif
#ifndef WLED_RELEASE_NAME
#warning WLED_RELEASE_NAME was not set - using default value of 'Custom'
#define WLED_RELEASE_NAME "Custom"
#endif
#ifndef WLED_REPO
// No warning for this one: integrators are not always on GitHub
#define WLED_REPO "unknown"
#endif
constexpr uint32_t WLED_CUSTOM_DESC_MAGIC = 0x57535453; // "WSTS" (WLED System Tag Structure)
constexpr uint32_t WLED_CUSTOM_DESC_VERSION = 1;
// Compile-time validation that release name doesn't exceed maximum length
static_assert(sizeof(WLED_RELEASE_NAME) <= WLED_RELEASE_NAME_MAX_LEN,
"WLED_RELEASE_NAME exceeds maximum length of WLED_RELEASE_NAME_MAX_LEN characters");
/**
* DJB2 hash function (C++11 compatible constexpr)
* Used for compile-time hash computation to validate structure contents
* Recursive for compile time: not usable at runtime due to stack depth
*
* Note that this only works on strings; there is no way to produce a compile-time
* hash of a struct in C++11 without explicitly listing all the struct members.
* So for now, we hash only the release name. This suffices for a "did you find
* valid structure" check.
*
*/
constexpr uint32_t djb2_hash_constexpr(const char* str, uint32_t hash = 5381) {
return (*str == '\0') ? hash : djb2_hash_constexpr(str + 1, ((hash << 5) + hash) + *str);
}
/**
* Runtime DJB2 hash function for validation
*/
inline uint32_t djb2_hash_runtime(const char* str) {
uint32_t hash = 5381;
while (*str) {
hash = ((hash << 5) + hash) + *str++;
}
return hash;
}
// ------------------------------------
// GLOBAL VARIABLES
// ------------------------------------
// Structure instantiation for this build
const wled_metadata_t __attribute__((section(BUILD_METADATA_SECTION))) WLED_BUILD_DESCRIPTION = {
WLED_CUSTOM_DESC_MAGIC, // magic
WLED_CUSTOM_DESC_VERSION, // version
TOSTRING(WLED_VERSION),
WLED_RELEASE_NAME, // release_name
std::integral_constant<uint32_t, djb2_hash_constexpr(WLED_RELEASE_NAME)>::value, // hash - computed at compile time; integral_constant enforces this
};
static const char repoString_s[] PROGMEM = WLED_REPO;
const __FlashStringHelper* repoString = FPSTR(repoString_s);
static const char productString_s[] PROGMEM = WLED_PRODUCT_NAME;
const __FlashStringHelper* productString = FPSTR(productString_s);
static const char brandString_s [] PROGMEM = WLED_BRAND;
const __FlashStringHelper* brandString = FPSTR(brandString_s);
/**
* Extract WLED custom description structure from binary
* @param binaryData Pointer to binary file data
* @param dataSize Size of binary data in bytes
* @param extractedDesc Buffer to store extracted custom description structure
* @return true if structure was found and extracted, false otherwise
*/
bool findWledMetadata(const uint8_t* binaryData, size_t dataSize, wled_metadata_t* extractedDesc) {
if (!binaryData || !extractedDesc || dataSize < sizeof(wled_metadata_t)) {
return false;
}
for (size_t offset = 0; offset <= dataSize - sizeof(wled_metadata_t); offset++) {
if ((binaryData[offset]) == static_cast<char>(WLED_CUSTOM_DESC_MAGIC)) {
// First byte matched; check next in an alignment-safe way
uint32_t data_magic;
memcpy(&data_magic, binaryData + offset, sizeof(data_magic));
// Check for magic number
if (data_magic == WLED_CUSTOM_DESC_MAGIC) {
wled_metadata_t candidate;
memcpy(&candidate, binaryData + offset, sizeof(candidate));
// Found potential match, validate version
if (candidate.desc_version != WLED_CUSTOM_DESC_VERSION) {
DEBUG_PRINTF_P(PSTR("Found WLED structure at offset %u but version mismatch: %u\n"),
offset, candidate.desc_version);
continue;
}
// Validate hash using runtime function
uint32_t expected_hash = djb2_hash_runtime(candidate.release_name);
if (candidate.hash != expected_hash) {
DEBUG_PRINTF_P(PSTR("Found WLED structure at offset %u but hash mismatch\n"), offset);
continue;
}
// Valid structure found - copy entire structure
*extractedDesc = candidate;
DEBUG_PRINTF_P(PSTR("Extracted WLED structure at offset %u: '%s'\n"),
offset, extractedDesc->release_name);
return true;
}
}
}
DEBUG_PRINTLN(F("No WLED custom description found in binary"));
return false;
}
/**
* Check if OTA should be allowed based on release compatibility using custom description
* @param binaryData Pointer to binary file data (not modified)
* @param dataSize Size of binary data in bytes
* @param errorMessage Buffer to store error message if validation fails
* @param errorMessageLen Maximum length of error message buffer
* @return true if OTA should proceed, false if it should be blocked
*/
bool shouldAllowOTA(const wled_metadata_t& firmwareDescription, char* errorMessage, size_t errorMessageLen) {
// Clear error message
if (errorMessage && errorMessageLen > 0) {
errorMessage[0] = '\0';
}
// Validate compatibility using extracted release name
// We make a stack copy so we can print it safely
char safeFirmwareRelease[WLED_RELEASE_NAME_MAX_LEN];
strncpy(safeFirmwareRelease, firmwareDescription.release_name, WLED_RELEASE_NAME_MAX_LEN - 1);
safeFirmwareRelease[WLED_RELEASE_NAME_MAX_LEN - 1] = '\0';
if (strlen(safeFirmwareRelease) == 0) {
return false;
}
if (strncmp_P(safeFirmwareRelease, releaseString, WLED_RELEASE_NAME_MAX_LEN) != 0) {
if (errorMessage && errorMessageLen > 0) {
snprintf_P(errorMessage, errorMessageLen, PSTR("Firmware compatibility mismatch: current='%s', uploaded='%s'."),
releaseString, safeFirmwareRelease);
errorMessage[errorMessageLen - 1] = '\0'; // Ensure null termination
}
return false;
}
// TODO: additional checks go here
return true;
}

61
wled00/wled_metadata.h Normal file
View File

@@ -0,0 +1,61 @@
/*
WLED build metadata
Manages and exports information about the current WLED build.
*/
#pragma once
#include <cstdint>
#include <string.h>
#include <WString.h>
#define WLED_VERSION_MAX_LEN 48
#define WLED_RELEASE_NAME_MAX_LEN 48
/**
* WLED Custom Description Structure
* This structure is embedded in platform-specific sections at an approximately
* fixed offset in ESP32/ESP8266 binaries, where it can be found and validated
* by the OTA process.
*/
typedef struct {
uint32_t magic; // Magic number to identify WLED custom description
uint32_t desc_version; // Structure version for future compatibility
char wled_version[WLED_VERSION_MAX_LEN];
char release_name[WLED_RELEASE_NAME_MAX_LEN]; // Release name (null-terminated)
uint32_t hash; // Structure sanity check
} __attribute__((packed)) wled_metadata_t;
// Global build description
extern const wled_metadata_t WLED_BUILD_DESCRIPTION;
// Convenient metdata pointers
#define versionString (WLED_BUILD_DESCRIPTION.wled_version) // Build version, WLED_VERSION
#define releaseString (WLED_BUILD_DESCRIPTION.release_name) // Release name, WLED_RELEASE_NAME
extern const __FlashStringHelper* repoString; // Github repository (if available)
extern const __FlashStringHelper* productString; // Product, WLED_PRODUCT_NAME -- deprecated, use WLED_RELEASE_NAME
extern const __FlashStringHelper* brandString ; // Brand
// Metadata analysis functions
/**
* Extract WLED custom description structure from binary data
* @param binaryData Pointer to binary file data
* @param dataSize Size of binary data in bytes
* @param extractedDesc Buffer to store extracted custom description structure
* @return true if structure was found and extracted, false otherwise
*/
bool findWledMetadata(const uint8_t* binaryData, size_t dataSize, wled_metadata_t* extractedDesc);
/**
* Check if OTA should be allowed based on release compatibility
* @param firmwareDescription Pointer to firmware description
* @param errorMessage Buffer to store error message if validation fails
* @param errorMessageLen Maximum length of error message buffer
* @return true if OTA should proceed, false if it should be blocked
*/
bool shouldAllowOTA(const wled_metadata_t& firmwareDescription, char* errorMessage, size_t errorMessageLen);

View File

@@ -1,11 +1,7 @@
#include "wled.h"
#ifndef WLED_DISABLE_OTA
#ifdef ESP8266
#include <Updater.h>
#else
#include <Update.h>
#endif
#include "ota_update.h"
#endif
#include "html_ui.h"
#include "html_settings.h"
@@ -17,6 +13,8 @@
#include "html_pxmagic.h"
#endif
#include "html_cpal.h"
#include "html_edit.h"
// define flash strings once (saves flash memory)
static const char s_redirecting[] PROGMEM = "Redirecting...";
@@ -26,8 +24,17 @@ static const char s_unlock_cfg [] PROGMEM = "Please unlock settings using PIN co
static const char s_rebooting [] PROGMEM = "Rebooting now...";
static const char s_notimplemented[] PROGMEM = "Not implemented";
static const char s_accessdenied[] PROGMEM = "Access Denied";
static const char s_not_found[] PROGMEM = "Not found";
static const char s_wsec[] PROGMEM = "wsec.json";
static const char s_func[] PROGMEM = "func";
static const char s_list[] PROGMEM = "list";
static const char s_path[] PROGMEM = "path";
static const char s_cache_control[] PROGMEM = "Cache-Control";
static const char s_no_store[] PROGMEM = "no-store";
static const char s_expires[] PROGMEM = "Expires";
static const char _common_js[] PROGMEM = "/common.js";
//Is this an IP?
static bool isIp(const String &str) {
for (size_t i = 0; i < str.length(); i++) {
@@ -60,7 +67,7 @@ static bool inLocalSubnet(const IPAddress &client) {
*/
static void generateEtag(char *etag, uint16_t eTagSuffix) {
sprintf_P(etag, PSTR("%7d-%02x-%04x"), VERSION, cacheInvalidate, eTagSuffix);
sprintf_P(etag, PSTR("%u-%02x-%04x"), WEB_BUILD_TIME, cacheInvalidate, eTagSuffix);
}
static void setStaticContentCacheHeaders(AsyncWebServerResponse *response, int code, uint16_t eTagSuffix = 0) {
@@ -71,9 +78,9 @@ static void setStaticContentCacheHeaders(AsyncWebServerResponse *response, int c
#ifndef WLED_DEBUG
// this header name is misleading, "no-cache" will not disable cache,
// it just revalidates on every load using the "If-None-Match" header with the last ETag value
response->addHeader(F("Cache-Control"), F("no-cache"));
response->addHeader(FPSTR(s_cache_control), F("no-cache"));
#else
response->addHeader(F("Cache-Control"), F("no-store,max-age=0")); // prevent caching if debug build
response->addHeader(FPSTR(s_cache_control), F("no-store,max-age=0")); // prevent caching if debug build
#endif
char etag[32];
generateEtag(etag, eTagSuffix);
@@ -176,6 +183,7 @@ static String msgProcessor(const String& var)
return String();
}
static void handleUpload(AsyncWebServerRequest *request, const String& filename, size_t index, uint8_t *data, size_t len, bool isFinal) {
if (!correctPIN) {
if (isFinal) request->send(401, FPSTR(CONTENT_TYPE_PLAIN), FPSTR(s_unlock_cfg));
@@ -198,7 +206,7 @@ static void handleUpload(AsyncWebServerRequest *request, const String& filename,
request->_tempFile.close();
if (filename.indexOf(F("cfg.json")) >= 0) { // check for filename with or without slash
doReboot = true;
request->send(200, FPSTR(CONTENT_TYPE_PLAIN), F("Configuration restore successful.\nRebooting..."));
request->send(200, FPSTR(CONTENT_TYPE_PLAIN), F("Config restore ok.\nRebooting..."));
} else {
if (filename.indexOf(F("palette")) >= 0 && filename.indexOf(F(".json")) >= 0) loadCustomPalettes();
request->send(200, FPSTR(CONTENT_TYPE_PLAIN), F("File Uploaded!"));
@@ -207,25 +215,98 @@ static void handleUpload(AsyncWebServerRequest *request, const String& filename,
}
}
void createEditHandler(bool enable) {
static const char _edit_htm[] PROGMEM = "/edit.htm";
void createEditHandler() {
if (editHandler != nullptr) server.removeHandler(editHandler);
if (enable) {
#ifdef WLED_ENABLE_FS_EDITOR
#ifdef ARDUINO_ARCH_ESP32
editHandler = &server.addHandler(new SPIFFSEditor(WLED_FS));//http_username,http_password));
#else
editHandler = &server.addHandler(new SPIFFSEditor("","",WLED_FS));//http_username,http_password));
#endif
#else
editHandler = &server.on(F("/edit"), HTTP_GET, [](AsyncWebServerRequest *request){
serveMessage(request, 501, FPSTR(s_notimplemented), F("The FS editor is disabled in this build."), 254);
});
#endif
} else {
editHandler = &server.on(F("/edit"), HTTP_ANY, [](AsyncWebServerRequest *request){
editHandler = &server.on(F("/edit"), static_cast<WebRequestMethod>(HTTP_GET), [](AsyncWebServerRequest *request) {
// PIN check for GET/DELETE, for POST it is done in handleUpload()
if (!correctPIN) {
serveMessage(request, 401, FPSTR(s_accessdenied), FPSTR(s_unlock_cfg), 254);
});
}
return;
}
const String& func = request->arg(FPSTR(s_func));
bool legacyList = false;
if (request->hasArg(FPSTR(s_list))) {
legacyList = true; // support for '?list=/'
}
if(func.length() == 0 && !legacyList) {
// default: serve the editor page
handleStaticContent(request, FPSTR(_edit_htm), 200, FPSTR(CONTENT_TYPE_HTML), PAGE_edit, PAGE_edit_length);
return;
}
if (func == FPSTR(s_list) || legacyList) {
bool first = true;
AsyncResponseStream* response = request->beginResponseStream(FPSTR(CONTENT_TYPE_JSON));
response->addHeader(FPSTR(s_cache_control), FPSTR(s_no_store));
response->addHeader(FPSTR(s_expires), F("0"));
response->write('[');
File rootdir = WLED_FS.open("/", "r");
File rootfile = rootdir.openNextFile();
while (rootfile) {
String name = rootfile.name();
if (name.indexOf(FPSTR(s_wsec)) >= 0) {
rootfile = rootdir.openNextFile(); // skip wsec.json
continue;
}
if (!first) response->write(',');
first = false;
response->printf_P(PSTR("{\"name\":\"%s\",\"type\":\"file\",\"size\":%u}"), name.c_str(), rootfile.size());
rootfile = rootdir.openNextFile();
}
rootfile.close();
rootdir.close();
response->write(']');
request->send(response);
return;
}
String path = request->arg(FPSTR(s_path)); // remaining functions expect a path
if (path.length() == 0) {
request->send(400, FPSTR(CONTENT_TYPE_PLAIN), F("Missing path"));
return;
}
if (path.charAt(0) != '/') {
path = '/' + path; // prepend slash if missing
}
if (!WLED_FS.exists(path)) {
request->send(404, FPSTR(CONTENT_TYPE_PLAIN), FPSTR(s_not_found));
return;
}
if (path.indexOf(FPSTR(s_wsec)) >= 0) {
request->send(403, FPSTR(CONTENT_TYPE_PLAIN), FPSTR(s_accessdenied)); // skip wsec.json
return;
}
if (func == "edit") {
request->send(WLED_FS, path);
return;
}
if (func == "download") {
request->send(WLED_FS, path, String(), true);
return;
}
if (func == "delete") {
if (!WLED_FS.remove(path))
request->send(500, FPSTR(CONTENT_TYPE_PLAIN), F("Delete failed"));
else
request->send(200, FPSTR(CONTENT_TYPE_PLAIN), F("File deleted"));
return;
}
// unrecognized func
request->send(400, FPSTR(CONTENT_TYPE_PLAIN), F("Invalid function"));
});
}
static bool captivePortal(AsyncWebServerRequest *request)
@@ -391,7 +472,7 @@ void initServer()
size_t len, bool isFinal) {handleUpload(request, filename, index, data, len, isFinal);}
);
createEditHandler(correctPIN);
createEditHandler(); // initialize "/edit" handler, access is protected by "correctPIN"
static const char _update[] PROGMEM = "/update";
#ifndef WLED_DISABLE_OTA
@@ -404,59 +485,47 @@ void initServer()
});
server.on(_update, HTTP_POST, [](AsyncWebServerRequest *request){
if (!correctPIN) {
serveSettings(request, true); // handle PIN page POST request
return;
}
if (otaLock) {
serveMessage(request, 401, FPSTR(s_accessdenied), FPSTR(s_unlock_ota), 254);
return;
}
if (Update.hasError()) {
serveMessage(request, 500, F("Update failed!"), F("Please check your file and retry!"), 254);
if (request->_tempObject) {
auto ota_result = getOTAResult(request);
if (ota_result.first) {
if (ota_result.second.length() > 0) {
serveMessage(request, 500, F("Update failed!"), ota_result.second, 254);
} else {
serveMessage(request, 200, F("Update successful!"), FPSTR(s_rebooting), 131);
}
}
} else {
serveMessage(request, 200, F("Update successful!"), FPSTR(s_rebooting), 131);
#ifndef ESP8266
bootloopCheckOTA(); // let the bootloop-checker know there was an OTA update
#endif
doReboot = true;
// No context structure - something's gone horribly wrong
serveMessage(request, 500, F("Update failed!"), F("Internal server fault"), 254);
}
},[](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool isFinal){
IPAddress client = request->client()->remoteIP();
if (((otaSameSubnet && !inSameSubnet(client)) && !strlen(settingsPIN)) || (!otaSameSubnet && !inLocalSubnet(client))) {
DEBUG_PRINTLN(F("Attempted OTA update from different/non-local subnet!"));
request->send(401, FPSTR(CONTENT_TYPE_PLAIN), FPSTR(s_accessdenied));
return;
}
if (!correctPIN || otaLock) return;
if(!index){
DEBUG_PRINTLN(F("OTA Update Start"));
#if WLED_WATCHDOG_TIMEOUT > 0
WLED::instance().disableWatchdog();
#endif
UsermodManager::onUpdateBegin(true); // notify usermods that update is about to begin (some may require task de-init)
lastEditTime = millis(); // make sure PIN does not lock during update
strip.suspend();
backupConfig(); // backup current config in case the update ends badly
strip.resetSegments(); // free as much memory as you can
#ifdef ESP8266
Update.runAsync(true);
#endif
Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000);
}
if(!Update.hasError()) Update.write(data, len);
if(isFinal){
if(Update.end(true)){
DEBUG_PRINTLN(F("Update Success"));
} else {
DEBUG_PRINTLN(F("Update Failed"));
strip.resume();
UsermodManager::onUpdateBegin(false); // notify usermods that update has failed (some may require task init)
#if WLED_WATCHDOG_TIMEOUT > 0
WLED::instance().enableWatchdog();
#endif
if (index == 0) {
// Allocate the context structure
if (!initOTA(request)) {
return; // Error will be dealt with after upload in response handler, above
}
// Privilege checks
IPAddress client = request->client()->remoteIP();
if (((otaSameSubnet && !inSameSubnet(client)) && !strlen(settingsPIN)) || (!otaSameSubnet && !inLocalSubnet(client))) {
DEBUG_PRINTLN(F("Attempted OTA update from different/non-local subnet!"));
serveMessage(request, 401, FPSTR(s_accessdenied), F("Client is not on local subnet."), 254);
setOTAReplied(request);
return;
}
if (!correctPIN) {
serveMessage(request, 401, FPSTR(s_accessdenied), FPSTR(s_unlock_cfg), 254);
setOTAReplied(request);
return;
};
if (otaLock) {
serveMessage(request, 401, FPSTR(s_accessdenied), FPSTR(s_unlock_ota), 254);
setOTAReplied(request);
return;
}
}
handleOTAData(request, index, data, len, isFinal);
});
#else
const auto notSupported = [](AsyncWebServerRequest *request){
@@ -466,6 +535,53 @@ void initServer()
server.on(_update, HTTP_POST, notSupported, [](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool isFinal){});
#endif
#if defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DISABLE_OTA)
// ESP32 bootloader update endpoint
server.on(F("/updatebootloader"), HTTP_POST, [](AsyncWebServerRequest *request){
if (request->_tempObject) {
auto bootloader_result = getBootloaderOTAResult(request);
if (bootloader_result.first) {
if (bootloader_result.second.length() > 0) {
serveMessage(request, 500, F("Bootloader update failed!"), bootloader_result.second, 254);
} else {
serveMessage(request, 200, F("Bootloader updated successfully!"), FPSTR(s_rebooting), 131);
}
}
} else {
// No context structure - something's gone horribly wrong
serveMessage(request, 500, F("Bootloader update failed!"), F("Internal server fault"), 254);
}
},[](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool isFinal){
if (index == 0) {
// Privilege checks
IPAddress client = request->client()->remoteIP();
if (((otaSameSubnet && !inSameSubnet(client)) && !strlen(settingsPIN)) || (!otaSameSubnet && !inLocalSubnet(client))) {
DEBUG_PRINTLN(F("Attempted bootloader update from different/non-local subnet!"));
serveMessage(request, 401, FPSTR(s_accessdenied), F("Client is not on local subnet."), 254);
setBootloaderOTAReplied(request);
return;
}
if (!correctPIN) {
serveMessage(request, 401, FPSTR(s_accessdenied), FPSTR(s_unlock_cfg), 254);
setBootloaderOTAReplied(request);
return;
}
if (otaLock) {
serveMessage(request, 401, FPSTR(s_accessdenied), FPSTR(s_unlock_ota), 254);
setBootloaderOTAReplied(request);
return;
}
// Allocate the context structure
if (!initBootloaderOTA(request)) {
return; // Error will be dealt with after upload in response handler, above
}
}
handleBootloaderOTAData(request, index, data, len, isFinal);
});
#endif
#ifdef WLED_ENABLE_DMX
server.on(F("/dmxmap"), HTTP_GET, [](AsyncWebServerRequest *request){
request->send_P(200, FPSTR(CONTENT_TYPE_HTML), PAGE_dmxmap, dmxProcessor);
@@ -569,8 +685,8 @@ void serveSettingsJS(AsyncWebServerRequest* request)
}
AsyncResponseStream *response = request->beginResponseStream(FPSTR(CONTENT_TYPE_JAVASCRIPT));
response->addHeader(F("Cache-Control"), F("no-store"));
response->addHeader(F("Expires"), F("0"));
response->addHeader(FPSTR(s_cache_control), FPSTR(s_no_store));
response->addHeader(FPSTR(s_expires), F("0"));
response->print(F("function GetV(){var d=document;"));
getSettingsJS(subPage, *response);
@@ -694,7 +810,6 @@ void serveSettings(AsyncWebServerRequest* request, bool post) {
#endif
case SUBPAGE_LOCK : {
correctPIN = !strlen(settingsPIN); // lock if a pin is set
createEditHandler(correctPIN);
serveMessage(request, 200, strlen(settingsPIN) > 0 ? PSTR("Settings locked") : PSTR("No PIN set"), FPSTR(s_redirecting), 1);
return;
}

View File

@@ -5,6 +5,12 @@
*/
#ifdef WLED_ENABLE_WEBSOCKETS
// define some constants for binary protocols, dont use defines but C++ style constexpr
constexpr uint8_t BINARY_PROTOCOL_GENERIC = 0xFF; // generic / auto detect NOT IMPLEMENTED
constexpr uint8_t BINARY_PROTOCOL_E131 = P_E131; // = 0, untested!
constexpr uint8_t BINARY_PROTOCOL_ARTNET = P_ARTNET; // = 1, untested!
constexpr uint8_t BINARY_PROTOCOL_DDP = P_DDP; // = 2
uint16_t wsLiveClientId = 0;
unsigned long wsLastLiveTime = 0;
//uint8_t* wsFrameBuffer = nullptr;
@@ -25,7 +31,7 @@ void wsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventTyp
// data packet
AwsFrameInfo * info = (AwsFrameInfo*)arg;
if(info->final && info->index == 0 && info->len == len){
// the whole message is in a single frame and we got all of its data (max. 1450 bytes)
// the whole message is in a single frame and we got all of its data (max. 1428 bytes / ESP8266: 528 bytes)
if(info->opcode == WS_TEXT)
{
if (len > 0 && len < 10 && data[0] == 'p') {
@@ -71,8 +77,29 @@ void wsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventTyp
// force broadcast in 500ms after updating client
//lastInterfaceUpdate = millis() - (INTERFACE_UPDATE_COOLDOWN -500); // ESP8266 does not like this
}
}else if (info->opcode == WS_BINARY) {
// first byte determines protocol. Note: since e131_packet_t is "packed", the compiler handles alignment issues
//DEBUG_PRINTF_P(PSTR("WS binary message: len %u, byte0: %u\n"), len, data[0]);
int offset = 1; // offset to skip protocol byte
switch (data[0]) {
case BINARY_PROTOCOL_E131:
handleE131Packet((e131_packet_t*)&data[offset], client->remoteIP(), P_E131);
break;
case BINARY_PROTOCOL_ARTNET:
handleE131Packet((e131_packet_t*)&data[offset], client->remoteIP(), P_ARTNET);
break;
case BINARY_PROTOCOL_DDP:
if (len < 10 + offset) return; // DDP header is 10 bytes (+1 protocol byte)
size_t ddpDataLen = (data[8+offset] << 8) | data[9+offset]; // data length in bytes from DDP header
uint8_t flags = data[0+offset];
if ((flags & DDP_TIMECODE_FLAG) ) ddpDataLen += 4; // timecode flag adds 4 bytes to data length
if (len < (10 + offset + ddpDataLen)) return; // not enough data, prevent out of bounds read
// could be a valid DDP packet, forward to handler
handleE131Packet((e131_packet_t*)&data[offset], client->remoteIP(), P_DDP);
}
}
} else {
DEBUG_PRINTF_P(PSTR("WS multipart message: final %u index %u len %u total %u\n"), info->final, info->index, len, (uint32_t)info->len);
//message is comprised of multiple frames or the frame is split into multiple packets
//if(info->index == 0){
//if (!wsFrameBuffer && len < 4096) wsFrameBuffer = new uint8_t[4096];

View File

@@ -291,7 +291,7 @@ void getSettingsJS(byte subPage, Print& settingsScript)
settingsScript.printf_P(PSTR("d.ledTypes=%s;"), BusManager::getLEDTypesJSONString().c_str());
// set limits
settingsScript.printf_P(PSTR("bLimits(%d,%d,%d,%d,%d,%d,%d,%d);"),
settingsScript.printf_P(PSTR("bLimits(%d,%d,%d,%d,%d,%d,%d,%d,%d);"),
WLED_MAX_BUSSES,
WLED_MIN_VIRTUAL_BUSSES, // irrelevant, but kept to distinguish S2/S3 in UI
MAX_LEDS_PER_BUS,
@@ -299,7 +299,8 @@ void getSettingsJS(byte subPage, Print& settingsScript)
MAX_LEDS,
WLED_MAX_COLOR_ORDER_MAPPINGS,
WLED_MAX_DIGITAL_CHANNELS,
WLED_MAX_ANALOG_CHANNELS
WLED_MAX_ANALOG_CHANNELS,
WLED_MAX_BUTTONS
);
printSetFormCheckbox(settingsScript,PSTR("MS"),strip.autoSegments);
@@ -403,8 +404,9 @@ void getSettingsJS(byte subPage, Print& settingsScript)
printSetFormValue(settingsScript,PSTR("RL"),rlyPin);
printSetFormCheckbox(settingsScript,PSTR("RM"),rlyMde);
printSetFormCheckbox(settingsScript,PSTR("RO"),rlyOpenDrain);
for (int i = 0; i < WLED_MAX_BUTTONS; i++) {
settingsScript.printf_P(PSTR("addBtn(%d,%d,%d);"), i, btnPin[i], buttonType[i]);
int i = 0;
for (const auto &button : buttons) {
settingsScript.printf_P(PSTR("addBtn(%d,%d,%d);"), i++, button.pin, button.type);
}
printSetFormCheckbox(settingsScript,PSTR("IP"),disablePullUp);
printSetFormValue(settingsScript,PSTR("TT"),touchThreshold);
@@ -578,8 +580,9 @@ void getSettingsJS(byte subPage, Print& settingsScript)
printSetFormValue(settingsScript,PSTR("A1"),macroAlexaOff);
printSetFormValue(settingsScript,PSTR("MC"),macroCountdown);
printSetFormValue(settingsScript,PSTR("MN"),macroNl);
for (unsigned i=0; i<WLED_MAX_BUTTONS; i++) {
settingsScript.printf_P(PSTR("addRow(%d,%d,%d,%d);"), i, macroButton[i], macroLongPress[i], macroDoublePress[i]);
int i = 0;
for (const auto &button : buttons) {
settingsScript.printf_P(PSTR("addRow(%d,%d,%d,%d);"), i++, button.macroButton, button.macroLongPress, button.macroDoublePress);
}
char k[4];
@@ -671,16 +674,6 @@ void getSettingsJS(byte subPage, Print& settingsScript)
UsermodManager::appendConfigData(settingsScript);
}
if (subPage == SUBPAGE_UPDATE) // update
{
char tmp_buf[128];
fillWLEDVersion(tmp_buf,sizeof(tmp_buf));
printSetClassElementHTML(settingsScript,PSTR("sip"),0,tmp_buf);
#ifndef ARDUINO_ARCH_ESP32
settingsScript.print(F("toggle('rev');")); // hide revert button on ESP8266
#endif
}
if (subPage == SUBPAGE_2D) // 2D matrices
{
printSetFormValue(settingsScript,PSTR("SOMP"),strip.isMatrix);