Compare commits

..

82 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
55d4d6c044 Revert incorrect gamma correction changes to loadCustomPalettes
Co-authored-by: softhack007 <91616163+softhack007@users.noreply.github.com>
2025-12-15 18:08:02 +00:00
copilot-swe-agent[bot]
d16dc5dfaa Complete fix for custom palette gamma correction issue
Co-authored-by: DedeHai <6280424+DedeHai@users.noreply.github.com>
2025-12-15 09:17:32 +00:00
copilot-swe-agent[bot]
0605c3b1ef Apply inverse gamma correction to custom palettes to match built-in palettes
Co-authored-by: DedeHai <6280424+DedeHai@users.noreply.github.com>
2025-12-15 09:16:02 +00:00
copilot-swe-agent[bot]
76616c02fd Initial plan 2025-12-15 09:05:03 +00:00
BobLoeffler68
bb6114e8aa PacMan effect (#4891)
* PacMan effect added
2025-12-14 12:36:51 +01:00
Will Tatam
c097cb1f27 Merge pull request #5185 from wled/copilot/add-repo-field-to-upgradedata
Add repo field to upgradeData in upgrade event reporting
2025-12-14 09:58:20 +00:00
Damian Schneider
9094b3130d Display gaps in peek, fix segment overflow bug (#5105)
- display gaps in peek 
- fix segment overflow bug when loading preset with larger bounds than current setup
2025-12-14 10:26:38 +01:00
Damian Schneider
32b104e1a9 Use sequential loading and requests for all UI resources (#5013)
* use sequential loading for all UI resources

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

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

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

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

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

plus minor name adjustment (adding "for WLED")


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

* Convert PSRAM from bytes to MB in usage reporting JavaScript

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

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

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

---------

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

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: DedeHai <6280424+DedeHai@users.noreply.github.com>
Co-authored-by: Aircoookie <21045690+Aircoookie@users.noreply.github.com>
2025-11-28 08:08:43 -05:00
Will 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
Damian Schneider
bc5d4fed3c remove early return from initconnection() 2025-09-02 19:46:02 +02:00
51 changed files with 3896 additions and 1311 deletions

View File

@@ -1,10 +1,6 @@
name: Usermod CI
on:
push:
paths:
- usermods/**
- .github/workflows/usermods.yml
pull_request:
paths:
- usermods/**
@@ -12,6 +8,8 @@ on:
jobs:
get_usermod_envs:
# Only run for pull requests from forks (not from branches within wled/WLED)
if: github.event.pull_request.head.repo.full_name != github.repository
name: Gather Usermods
runs-on: ubuntu-latest
steps:
@@ -31,6 +29,8 @@ jobs:
build:
# Only run for pull requests from forks (not from branches within wled/WLED)
if: github.event.pull_request.head.repo.full_name != github.repository
name: Build Enviornments
runs-on: ubuntu-latest
needs: get_usermod_envs

View File

@@ -26,9 +26,28 @@ Github will pick up the changes so your PR stays up-to-date.
> It has many subtle and unexpected consequences on our github reposistory.
> For example, we regularly lost review comments when the PR author force-pushes code changes. So, pretty please, do not force-push.
> [!TIP]
> use [cherry-picking](https://docs.github.com/en/desktop/managing-commits/cherry-picking-a-commit-in-github-desktop) to copy commits from one branch to another.
You can find a collection of very useful tips and tricks here: https://github.com/wled-dev/WLED/wiki/How-to-properly-submit-a-PR
### Source Code from an AI agent or bot
> [!IMPORTANT]
> Its OK if you took help from an AI for writing your source code.
>
> However, we expect a few things from you as the person making a contribution to WLED:
* Make sure you really understand the code suggested by the AI, and don't just accept it because it "seems to work".
* Don't let the AI change existing code without double-checking by you as the contributor. Often, the result will not be complete. For example, previous source code comments may be lost.
* Remember that AI are still "Often-Wrong" ;-)
* If you don't feel very confident using English, you can use AI for translating code comments and descriptions into English. AI bots are very good at understanding language. However, always check if the results is correct. The translation might still have wrong technical terms, or errors in some details.
#### best practice with AI:
* As the person who contributes source code to WLED, make sure you understand exactly what the AI generated code does
* best practice: add a comment like ``'// below section of my code was generated by an AI``, when larger parts of your source code were not written by you personally.
* always review translations and code comments for correctness
* always review AI generated source code
* If the AI has rewritten existing code, check that the change is necessary and that nothing has been lost or broken. Also check that previous code comments are still intact.
### Code style

View File

@@ -2,7 +2,7 @@
"build": {
"arduino":{
"ldscript": "esp32s3_out.ld",
"partitions": "partitions-8MB-tinyuf2.csv"
"partitions": "default_8MB.csv"
},
"core": "esp32",
"extra_flags": [
@@ -43,16 +43,8 @@
"arduino",
"espidf"
],
"name": "Adafruit MatrixPortal ESP32-S3",
"name": "Adafruit MatrixPortal ESP32-S3 for WLED",
"upload": {
"arduino": {
"flash_extra_images": [
[
"0x410000",
"variants/adafruit_matrixportal_esp32s3/tinyuf2.bin"
]
]
},
"flash_size": "8MB",
"maximum_ram_size": 327680,
"maximum_size": 8388608,

View File

@@ -14,14 +14,14 @@
},
"repository": {
"type": "git",
"url": "git+https://github.com/wled-dev/WLED.git"
"url": "git+https://github.com/wled/WLED.git"
},
"author": "",
"license": "ISC",
"bugs": {
"url": "https://github.com/wled-dev/WLED/issues"
"url": "https://github.com/wled/WLED/issues"
},
"homepage": "https://github.com/wled-dev/WLED#readme",
"homepage": "https://github.com/wled/WLED#readme",
"dependencies": {
"clean-css": "^5.3.3",
"html-minifier-terser": "^7.2.0",
@@ -31,4 +31,4 @@
"engines": {
"node": ">=20.0.0"
}
}
}

View File

@@ -160,7 +160,7 @@ lib_compat_mode = strict
lib_deps =
fastled/FastLED @ 3.6.0
IRremoteESP8266 @ 2.8.2
makuna/NeoPixelBus @ 2.8.3
https://github.com/Makuna/NeoPixelBus.git#a0919d1c10696614625978dd6fb750a1317a14ce
https://github.com/Aircoookie/ESPAsyncWebServer.git#v2.4.2
marvinroger/AsyncMqttClient @ 0.9.0
# for I2C interface
@@ -256,7 +256,7 @@ lib_deps_compat =
lib_deps =
esp32async/AsyncTCP @ 3.4.7
bitbank2/AnimatedGIF@^1.4.7
https://github.com/Aircoookie/GifDecoder#bc3af18
https://github.com/Aircoookie/GifDecoder.git#bc3af189b6b1e06946569f6b4287f0b79a860f8e
build_flags =
-D CONFIG_ASYNC_TCP_USE_WDT=0
-D CONFIG_ASYNC_TCP_STACK_SIZE=8192
@@ -300,7 +300,7 @@ build_flags = -g
-D WLED_ENABLE_DMX_INPUT
lib_deps =
${esp32_all_variants.lib_deps}
https://github.com/someweisguy/esp_dmx.git#47db25d
https://github.com/someweisguy/esp_dmx.git#47db25d8c515e76fabcf5fc5ab0b786f98eeade0
${env.lib_deps}
[esp32s2]

View File

@@ -580,7 +580,7 @@ build_flags = ${common.build_flags}
[env:adafruit_matrixportal_esp32s3]
; ESP32-S3 processor, 8 MB flash, 2 MB of PSRAM, dedicated driver pins for HUB75
board = adafruit_matrixportal_esp32s3
board = adafruit_matrixportal_esp32s3_wled ; modified board definition: removed flash section that causes FS erase on upload
;; adafruit recommends to use arduino-esp32 2.0.14
;;platform = espressif32@ ~6.5.0
;;platform_packages = platformio/framework-arduinoespressif32 @ 3.20014.231204 ;; arduino-esp32 2.0.14

View File

@@ -26,7 +26,7 @@ const packageJson = require("../package.json");
// Export functions for testing
module.exports = { isFileNewerThan, isAnyFileInFolderNewerThan };
const output = ["wled00/html_ui.h", "wled00/html_pixart.h", "wled00/html_cpal.h", "wled00/html_edit.h", "wled00/html_pxmagic.h", "wled00/html_settings.h", "wled00/html_other.h"]
const output = ["wled00/html_ui.h", "wled00/html_pixart.h", "wled00/html_cpal.h", "wled00/html_edit.h", "wled00/html_pxmagic.h", "wled00/html_pixelforge.h", "wled00/html_settings.h", "wled00/html_other.h"]
// \x1b[34m is blue, \x1b[36m is cyan, \x1b[0m is reset
const wledBanner = `
@@ -38,6 +38,11 @@ const wledBanner = `
\t\t\x1b[36m build script for web UI
\x1b[0m`;
// Generate build timestamp as UNIX timestamp (seconds since epoch)
function generateBuildTime() {
return Math.floor(Date.now() / 1000);
}
const singleHeader = `/*
* Binary array for the Web UI.
* gzip is used for smaller size and improved speeds.
@@ -45,6 +50,9 @@ const singleHeader = `/*
* Please see https://kno.wled.ge/advanced/custom-features/#changing-web-ui
* to find out how to easily modify the web UI source!
*/
// Automatically generated build time for cache busting (UNIX timestamp)
#define WEB_BUILD_TIME ${generateBuildTime()}
`;
@@ -126,12 +134,13 @@ async function minify(str, type = "plain") {
throw new Error("Unknown filter: " + type);
}
async function writeHtmlGzipped(sourceFile, resultFile, page) {
async function writeHtmlGzipped(sourceFile, resultFile, page, inlineCss = true) {
console.info("Reading " + sourceFile);
inline.html({
fileContent: fs.readFileSync(sourceFile, "utf8"),
relativeTo: path.dirname(sourceFile),
strict: true,
strict: inlineCss, // when not inlining css, ignore errors (enables linking style.css from subfolder htm files)
stylesheets: inlineCss // when true (default), css is inlined
},
async function (error, html) {
if (error) throw error;
@@ -244,8 +253,8 @@ if (isAlreadyBuilt("wled00/data") && process.argv[2] !== '--force' && process.ar
writeHtmlGzipped("wled00/data/index.htm", "wled00/html_ui.h", 'index');
writeHtmlGzipped("wled00/data/pixart/pixart.htm", "wled00/html_pixart.h", 'pixart');
//writeHtmlGzipped("wled00/data/cpal/cpal.htm", "wled00/html_cpal.h", 'cpal');
writeHtmlGzipped("wled00/data/pxmagic/pxmagic.htm", "wled00/html_pxmagic.h", 'pxmagic');
writeHtmlGzipped("wled00/data/pixelforge/pixelforge.htm", "wled00/html_pixelforge.h", 'pixelforge', false); // do not inline css
//writeHtmlGzipped("wled00/data/edit.htm", "wled00/html_edit.h", 'edit');

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

@@ -1335,7 +1335,7 @@ class AudioReactive : public Usermod {
disableSoundProcessing = true;
} else {
#if defined(ARDUINO_ARCH_ESP32) && defined(WLED_DEBUG)
if ((disableSoundProcessing == true) && (audioSyncEnabled == 0) && audioSource->isInitialized()) { // we just switched to "enabled"
if ((disableSoundProcessing == true) && (audioSyncEnabled == 0) && audioSource && audioSource->isInitialized()) { // we just switched to "enabled"
DEBUG_PRINTLN(F("[AR userLoop] realtime mode ended - audio processing resumed."));
DEBUG_PRINTF_P(PSTR(" RealtimeMode = %d; RealtimeOverride = %d\n"), int(realtimeMode), int(realtimeOverride));
}
@@ -1347,7 +1347,7 @@ class AudioReactive : public Usermod {
if (audioSyncEnabled & 0x02) disableSoundProcessing = true; // make sure everything is disabled IF in audio Receive mode
if (audioSyncEnabled & 0x01) disableSoundProcessing = false; // keep running audio IF we're in audio Transmit mode
#ifdef ARDUINO_ARCH_ESP32
if (!audioSource->isInitialized()) disableSoundProcessing = true; // no audio source
if (!audioSource || !audioSource->isInitialized()) disableSoundProcessing = true; // no audio source
// Only run the sampling code IF we're not in Receive mode or realtime mode
@@ -1544,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

@@ -25,51 +25,11 @@ class StairwayWipeUsermod : public Usermod {
public:
void setup() {
}
/**
* @brief Drives the stairway wipe state machine and reacts to user variables.
*
* @details
* Reads userVar0 (U0) and userVar1 (U1) to control a directional stairway color wipe:
* - U0 = 0: off.
* - U0 = 1: start/keep wipe from local side.
* - U0 = 2: start/keep wipe from opposite side.
* - U0 = 3: toggle mode for direction 1 (becomes 1 when off, 0 when on).
* - U0 = 4: toggle mode for direction 2 (becomes 2 when off, 0 when on).
*
* Manages a small state machine:
* - State 0: idle, will start a wipe.
* - State 1: wiping; transitions to static when wipe completes.
* - State 2: static/hold; transitions to off after U1 seconds if U1 > 0.
* - State 3: prepare to wipe off (or immediately off if off-wipe is disabled).
* - State 4: wiping off; turns fully off when wipe-off completes.
*
* The wipe duration and wipe-off timing are derived from the current effectSpeed. A change
* in trigger side (previousUserVar0 differing from userVar0) forces the usermod to begin
* turning off. When turning on/off the code invokes startWipe() or turnOff() and issues
* color/state update notifications as appropriate.
*
* @note Defining STAIRCASE_WIPE_OFF enables a reverse color-wipe transition when turning off;
* without it the lights fade off immediately.
*/
void loop() {
void loop() {
//userVar0 (U0 in HTTP API):
//has to be set to 1 if movement is detected on the PIR that is the same side of the staircase as the ESP8266
//has to be set to 2 if movement is detected on the PIR that is the opposite side
//can be set to 0 if no movement is detected. Otherwise LEDs will turn off after a configurable timeout (userVar1 seconds)
//U0 = 3: Toggle mode for direction 1 (if off, turn on with U0=1; if on, turn off with U0=0)
//U0 = 4: Toggle mode for direction 2 (if off, turn on with U0=2; if on, turn off with U0=0)
// Handle toggle modes U0=3 and U0=4
if (userVar0 == 3 || userVar0 == 4) {
if (wipeState == 0 || wipeState == 3 || wipeState == 4) {
// Lights are off or turning off, so turn them on
wipeState = 0; // Reset state so the state machine starts fresh
userVar0 = (userVar0 == 3) ? 1 : 2;
} else {
// Lights are on or turning on, so turn them off
userVar0 = 0;
}
}
if (userVar0 > 0)
{

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
//////////////
@@ -161,7 +172,7 @@ uint16_t mode_copy_segment(void) {
} else { // 1D source, source can be expanded into 2D
for (unsigned i = 0; i < SEGMENT.vLength(); i++) {
if(SEGMENT.check2) {
sourcecolor = strip.getPixelColor(i + sourcesegment.start); // read from global buffer (reads the last rendered frame)
sourcecolor = strip.getPixelColorNoMap(i + sourcesegment.start); // read from global buffer (reads the last rendered frame)
}
else {
sourcesegment.setDrawDimensions(); // set to source segment dimensions
@@ -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,203 @@ 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
/*
/ Pac-Man by Bob Loeffler with help from @dedehai and @blazoncek
* speed slider is for speed.
* intensity slider is for selecting the number of power dots.
* custom1 slider is for selecting the LED where the ghosts will start blinking blue.
* custom2 slider is for blurring the LEDs in the segment.
* custom3 slider is for selecting the # of ghosts (between 2 and 8).
* check1 is for displaying White Dots that PacMan eats. Enabled will show white dots. Disabled will not show any white dots (all leds will be black).
* check2 is for Smear mode (enabled will smear/persist the LED colors, disabled will not).
* check3 is for the Compact Dots mode of displaying white dots. Enabled will show white dots in every LED. Disabled will show black LEDs between the white dots.
* aux0 is used to keep track of the previous number of power dots in case the user selects a different number with the intensity slider.
* aux1 is the main counter for timing.
*/
typedef struct PacManChars {
signed pos;
signed topPos; // LED position of farthest PacMan has moved
uint32_t color;
bool direction; // true = moving away from first LED
bool blue; // used for ghosts only
bool eaten; // used for power dots only
} pacmancharacters_t;
static uint16_t mode_pacman(void) {
constexpr unsigned ORANGEYELLOW = 0xFFCC00;
constexpr unsigned PURPLEISH = 0xB000B0;
constexpr unsigned ORANGEISH = 0xFF8800;
constexpr unsigned WHITEISH = 0x999999;
constexpr unsigned PACMAN = 0; // PacMan is character[0]
constexpr uint32_t ghostColors[] = {RED, PURPLEISH, CYAN, ORANGEISH};
unsigned maxPowerDots = min(SEGLEN / 10U, 255U); // cap the max so packed state fits in 8 bits
unsigned numPowerDots = map(SEGMENT.intensity, 0, 255, 1, maxPowerDots);
unsigned numGhosts = map(SEGMENT.custom3, 0, 31, 2, 8);
bool smearMode = SEGMENT.check2;
// Pack two 8-bit values into one 16-bit field (stored in SEGENV.aux0)
uint16_t combined_value = uint16_t(((numPowerDots & 0xFF) << 8) | (numGhosts & 0xFF));
if (combined_value != SEGENV.aux0) SEGENV.call = 0; // Reinitialize on setting change
SEGENV.aux0 = combined_value;
// Allocate segment data
unsigned dataSize = sizeof(pacmancharacters_t) * (numGhosts + maxPowerDots + 1); // +1 is the PacMan character
if (SEGLEN <= 16 + (2*numGhosts) || !SEGENV.allocateData(dataSize)) return mode_static();
pacmancharacters_t *character = reinterpret_cast<pacmancharacters_t *>(SEGENV.data);
// Calculate when blue ghosts start blinking.
// On first call (or after settings change), `topPos` is not known yet, so fall back to the full segment length in that case.
int maxBlinkPos = (SEGENV.call == 0) ? (int)SEGLEN - 1 : character[PACMAN].topPos;
if (maxBlinkPos < 20) maxBlinkPos = 20;
int startBlinkingGhostsLED = (SEGLEN < 64)
? (int)SEGLEN / 3
: map(SEGMENT.custom1, 0, 255, 20, maxBlinkPos);
// Initialize characters on first call
if (SEGENV.call == 0) {
// Initialize PacMan
character[PACMAN].color = YELLOW;
character[PACMAN].pos = 0;
character[PACMAN].topPos = 0;
character[PACMAN].direction = true;
character[PACMAN].blue = false;
// Initialize ghosts with alternating colors
for (int i = 1; i <= numGhosts; i++) {
character[i].color = ghostColors[(i-1) % 4];
character[i].pos = -2 * (i + 1);
character[i].direction = true;
character[i].blue = false;
}
// Initialize power dots
for (int i = 0; i < numPowerDots; i++) {
character[i + numGhosts + 1].color = ORANGEYELLOW;
character[i + numGhosts + 1].eaten = false;
}
character[numGhosts + 1].pos = SEGLEN - 1; // Last power dot at end
}
if (strip.now > SEGENV.step) {
SEGENV.step = strip.now;
SEGENV.aux1++;
}
// Clear background if not in smear mode
if (!smearMode) SEGMENT.fill(BLACK);
// Draw white dots in front of PacMan if option selected
if (SEGMENT.check1) {
int step = SEGMENT.check3 ? 1 : 2; // Compact or spaced dots
for (int i = SEGLEN - 1; i > character[PACMAN].topPos; i -= step) {
SEGMENT.setPixelColor(i, WHITEISH);
}
}
// Update power dot positions dynamically
uint32_t everyXLeds = (((uint32_t)SEGLEN - 10U) << 8) / numPowerDots; // Fixed-point spacing for power dots: use 32-bit math to avoid overflow on long segments.
for (int i = 1; i < numPowerDots; i++) {
character[i + numGhosts + 1].pos = 10 + ((i * everyXLeds) >> 8);
}
// Blink power dots every 10 ticks
if (SEGENV.aux1 % 10 == 0) {
uint32_t dotColor = (character[numGhosts + 1].color == ORANGEYELLOW) ? BLACK : ORANGEYELLOW;
for (int i = 0; i < numPowerDots; i++) {
character[i + numGhosts + 1].color = dotColor;
}
}
// Blink blue ghosts when nearing start
if (SEGENV.aux1 % 15 == 0 && character[1].blue && character[PACMAN].pos <= startBlinkingGhostsLED) {
uint32_t ghostColor = (character[1].color == BLUE) ? WHITEISH : BLUE;
for (int i = 1; i <= numGhosts; i++) {
character[i].color = ghostColor;
}
}
// Draw uneaten power dots
for (int i = 0; i < numPowerDots; i++) {
if (!character[i + numGhosts + 1].eaten && (unsigned)character[i + numGhosts + 1].pos < SEGLEN) {
SEGMENT.setPixelColor(character[i + numGhosts + 1].pos, character[i + numGhosts + 1].color);
}
}
// Check if PacMan ate a power dot
for (int j = 0; j < numPowerDots; j++) {
auto &dot = character[j + numGhosts + 1];
if (character[PACMAN].pos == dot.pos && !dot.eaten) {
// Reverse all characters - PacMan now chases ghosts
for (int i = 0; i <= numGhosts; i++) {
character[i].direction = false;
}
// Turn ghosts blue
for (int i = 1; i <= numGhosts; i++) {
character[i].color = BLUE;
character[i].blue = true;
}
dot.eaten = true;
break; // only one power dot per frame
}
}
// Reset when PacMan reaches start with blue ghosts
if (character[1].blue && character[PACMAN].pos <= 0) {
// Reverse direction back
for (int i = 0; i <= numGhosts; i++) {
character[i].direction = true;
}
// Reset ghost colors
for (int i = 1; i <= numGhosts; i++) {
character[i].color = ghostColors[(i-1) % 4];
character[i].blue = false;
}
// Reset power dots if last one was eaten
if (character[numGhosts + 1].eaten) {
for (int i = 0; i < numPowerDots; i++) {
character[i + numGhosts + 1].eaten = false;
}
character[PACMAN].topPos = 0; // set the top position of PacMan to LED 0 (beginning of the segment)
}
}
// Update and draw characters based on speed setting
bool updatePositions = (SEGENV.aux1 % map(SEGMENT.speed, 0, 255, 15, 1) == 0);
// update positions of characters if it's time to do so
if (updatePositions) {
character[PACMAN].pos += character[PACMAN].direction ? 1 : -1;
for (int i = 1; i <= numGhosts; i++) {
character[i].pos += character[i].direction ? 1 : -1;
}
}
// Draw PacMan
if ((unsigned)character[PACMAN].pos < SEGLEN) {
SEGMENT.setPixelColor(character[PACMAN].pos, character[PACMAN].color);
}
// Draw ghosts
for (int i = 1; i <= numGhosts; i++) {
if ((unsigned)character[i].pos < SEGLEN) {
SEGMENT.setPixelColor(character[i].pos, character[i].color);
}
}
// Track farthest position of PacMan
if (character[PACMAN].topPos < character[PACMAN].pos) {
character[PACMAN].topPos = character[PACMAN].pos;
}
SEGMENT.blur(SEGMENT.custom2>>1);
return FRAMETIME;
}
static const char _data_FX_MODE_PACMAN[] PROGMEM = "PacMan@Speed,# of PowerDots,Blink distance,Blur,# of Ghosts,Dots,Smear,Compact;;!;1;m12=0,sx=192,ix=64,c1=64,c2=0,c3=12,o1=1,o2=0";
/*
* Sinelon stolen from FASTLED examples
@@ -3213,7 +3429,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 +3633,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 +3765,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 +3905,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 +4553,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 +4687,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.
@@ -6033,7 +6248,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 //
/////////////////////////
@@ -6221,7 +6436,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 //
@@ -8224,7 +8439,7 @@ uint16_t mode_particlefire(void) {
uint32_t numFlames; // number of flames: depends on fire width. for a fire width of 16 pixels, about 25-30 flames give good results
if (SEGMENT.call == 0) { // initialization
if (!initParticleSystem2D(PartSys, SEGMENT.virtualWidth(), 4)) //maximum number of source (PS may limit based on segment size); need 4 additional bytes for time keeping (uint32_t lastcall)
if (!initParticleSystem2D(PartSys, SEGMENT.vWidth(), 4)) //maximum number of source (PS may limit based on segment size); need 4 additional bytes for time keeping (uint32_t lastcall)
return mode_static(); // allocation failed or not 2D
SEGENV.aux0 = hw_random16(); // aux0 is wind position (index) in the perlin noise
}
@@ -8264,10 +8479,10 @@ uint16_t mode_particlefire(void) {
PartSys->sources[i].source.x = (PartSys->maxX >> 1) - (spread >> 1) + hw_random(spread); // change flame position: distribute randomly on chosen width
PartSys->sources[i].source.y = -(PS_P_RADIUS << 2); // set the source below the frame
PartSys->sources[i].source.ttl = 20 + hw_random16((SEGMENT.custom1 * SEGMENT.custom1) >> 8) / (1 + (firespeed >> 5)); //'hotness' of fire, faster flames reduce the effect or flame height will scale too much with speed
PartSys->sources[i].maxLife = hw_random16(SEGMENT.virtualHeight() >> 1) + 16; // defines flame height together with the vy speed, vy speed*maxlife/PS_P_RADIUS is the average flame height
PartSys->sources[i].maxLife = hw_random16(SEGMENT.vHeight() >> 1) + 16; // defines flame height together with the vy speed, vy speed*maxlife/PS_P_RADIUS is the average flame height
PartSys->sources[i].minLife = PartSys->sources[i].maxLife >> 1;
PartSys->sources[i].vx = hw_random16(5) - 2; // emitting speed (sideways)
PartSys->sources[i].vy = (SEGMENT.virtualHeight() >> 1) + (firespeed >> 4) + (SEGMENT.custom1 >> 4); // emitting speed (upwards)
PartSys->sources[i].vy = (SEGMENT.vHeight() >> 1) + (firespeed >> 4) + (SEGMENT.custom1 >> 4); // emitting speed (upwards)
PartSys->sources[i].var = 2 + hw_random16(2 + (firespeed >> 4)); // speed variation around vx,vy (+/- var)
}
}
@@ -8297,11 +8512,11 @@ uint16_t mode_particlefire(void) {
if(hw_random8() < 10 + (SEGMENT.intensity >> 2)) {
for (i = 0; i < PartSys->usedParticles; i++) {
if (PartSys->particles[i].ttl == 0) { // find a dead particle
PartSys->particles[i].ttl = hw_random16(SEGMENT.virtualHeight()) + 30;
PartSys->particles[i].ttl = hw_random16(SEGMENT.vHeight()) + 30;
PartSys->particles[i].x = PartSys->sources[0].source.x;
PartSys->particles[i].y = PartSys->sources[0].source.y;
PartSys->particles[i].vx = PartSys->sources[0].source.vx;
PartSys->particles[i].vy = (SEGMENT.virtualHeight() >> 1) + (firespeed >> 4) + ((30 + (SEGMENT.intensity >> 1) + SEGMENT.custom1) >> 4); // emitting speed (upwards)
PartSys->particles[i].vy = (SEGMENT.vHeight() >> 1) + (firespeed >> 4) + ((30 + (SEGMENT.intensity >> 1) + SEGMENT.custom1) >> 4); // emitting speed (upwards)
break; // emit only one particle
}
}
@@ -8367,11 +8582,12 @@ uint16_t mode_particlepit(void) {
PartSys->particles[i].sat = ((SEGMENT.custom3) << 3) + 7;
// set particle size
if (SEGMENT.custom1 == 255) {
PartSys->setParticleSize(1); // set global size to 1 for advanced rendering (no single pixel particles)
PartSys->perParticleSize = true;
PartSys->advPartProps[i].size = hw_random16(SEGMENT.custom1); // set each particle to random size
} else {
PartSys->perParticleSize = false;
PartSys->setParticleSize(SEGMENT.custom1); // set global size
PartSys->advPartProps[i].size = 0; // use global size
PartSys->advPartProps[i].size = SEGMENT.custom1; // also set individual size for consistency
}
break; // emit only one particle per round
}
@@ -8389,7 +8605,7 @@ uint16_t mode_particlepit(void) {
return FRAMETIME;
}
static const char _data_FX_MODE_PARTICLEPIT[] PROGMEM = "PS Ballpit@Speed,Intensity,Size,Hardness,Saturation,Cylinder,Walls,Ground;;!;2;pal=11,sx=100,ix=220,c1=120,c2=130,c3=31,o3=1";
static const char _data_FX_MODE_PARTICLEPIT[] PROGMEM = "PS Ballpit@Speed,Intensity,Size,Hardness,Saturation,Cylinder,Walls,Ground;;!;2;pal=11,sx=100,ix=220,c1=70,c2=180,c3=31,o3=1";
/*
Particle Waterfall
@@ -8473,7 +8689,7 @@ uint16_t mode_particlebox(void) {
uint32_t i;
if (SEGMENT.call == 0) { // initialization
if (!initParticleSystem2D(PartSys, 1)) // init
if (!initParticleSystem2D(PartSys, 1, 0, true)) // init
return mode_static(); // allocation failed or not 2D
PartSys->setBounceX(true);
PartSys->setBounceY(true);
@@ -8486,19 +8702,26 @@ uint16_t mode_particlebox(void) {
return mode_static(); // something went wrong, no data!
PartSys->updateSystem(); // update system properties (dimensions and data pointers)
PartSys->setParticleSize(SEGMENT.custom3<<3);
PartSys->setWallHardness(min(SEGMENT.custom2, (uint8_t)200)); // wall hardness is 200 or more
PartSys->enableParticleCollisions(true, max(2, (int)SEGMENT.custom2)); // enable collisions and set particle collision hardness
PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 2, 153)); // 1% - 60%
int maxParticleSize = min(((SEGMENT.vWidth() * SEGMENT.vHeight()) >> 2), 255U); // max particle size based on matrix size
unsigned currentParticleSize = map(SEGMENT.custom3, 0, 31, 0, maxParticleSize);
PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 2, 153) / (1 + (currentParticleSize >> 4))); // 1% - 60%, reduce if using larger size
if (SEGMENT.custom3 < 31)
PartSys->setParticleSize(currentParticleSize); // set global size if not max (resets perParticleSize)
else
PartSys->perParticleSize = true; // per particle size, uses advPartProps.size (randomized below)
// add in new particles if amount has changed
for (i = 0; i < PartSys->usedParticles; i++) {
if (PartSys->particles[i].ttl < 260) { // initialize handed over particles and dead particles
if (PartSys->particles[i].ttl < 260) { // initialize dead particles
PartSys->particles[i].ttl = 260; // full brigthness
PartSys->particles[i].x = hw_random16(PartSys->maxX);
PartSys->particles[i].y = hw_random16(PartSys->maxY);
PartSys->particles[i].hue = hw_random8(); // make it colorful
PartSys->particleFlags[i].perpetual = true; // never die
PartSys->particleFlags[i].collide = true; // all particles colllide
PartSys->advPartProps[i].size = hw_random8(maxParticleSize); // random size, used only if size is set to max (SEGMENT.custom3=31)
break; // only spawn one particle per frame for less chaotic transitions
}
}
@@ -8754,22 +8977,10 @@ uint16_t mode_particleattractor(void) {
// Particle System settings
PartSys->updateSystem(); // update system properties (dimensions and data pointers)
attractor = reinterpret_cast<PSparticle *>(PartSys->PSdataEnd);
PartSys->setColorByAge(SEGMENT.check1);
PartSys->setParticleSize(SEGMENT.custom1 >> 1); //set size globally
PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 25, 190));
if (SEGMENT.custom2 > 0) // collisions enabled
PartSys->enableParticleCollisions(true, map(SEGMENT.custom2, 1, 255, 120, 255)); // enable collisions and set particle collision hardness
else
PartSys->enableParticleCollisions(false);
if (SEGMENT.call == 0) {
attractor->vx = PartSys->sources[0].source.vy; // set to spray movemement but reverse x and y
attractor->vy = PartSys->sources[0].source.vx;
}
attractor = reinterpret_cast<PSparticle *>(PartSys->PSdataEnd);
// set attractor properties
attractor->ttl = 100; // never dies
if (SEGMENT.check2) {
@@ -8780,6 +8991,15 @@ uint16_t mode_particleattractor(void) {
attractor->x = PartSys->maxX >> 1; // set to center
attractor->y = PartSys->maxY >> 1;
}
if (SEGMENT.call == 0) {
attractor->vx = PartSys->sources[0].source.vy; // set to spray movemement but reverse x and y
attractor->vy = PartSys->sources[0].source.vx;
}
if (SEGMENT.custom2 > 0) // collisions enabled
PartSys->enableParticleCollisions(true, map(SEGMENT.custom2, 1, 255, 120, 255)); // enable collisions and set particle collision hardness
else
PartSys->enableParticleCollisions(false);
if (SEGMENT.call % 5 == 0)
PartSys->sources[0].source.hue++;
@@ -8791,13 +9011,11 @@ uint16_t mode_particleattractor(void) {
PartSys->angleEmit(PartSys->sources[0], SEGENV.aux0 + 0x7FFF, 12); // emit at 180° as well
// apply force
uint32_t strength = SEGMENT.speed;
#ifdef USERMOD_AUDIOREACTIVE
um_data_t *um_data;
if (UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { // AR active, do not use simulated data
uint32_t volumeSmth = (uint32_t)(*(float*) um_data->u_data[0]); // 0-255
strength = (SEGMENT.speed * volumeSmth) >> 8;
}
#endif
for (uint32_t i = 0; i < PartSys->usedParticles; i++) {
PartSys->pointAttractor(i, *attractor, strength, SEGMENT.check3);
}
@@ -8809,6 +9027,7 @@ uint16_t mode_particleattractor(void) {
PartSys->update(); // update and render
return FRAMETIME;
}
//static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "PS Attractor@Mass,Particles,Size,Collide,Friction,AgeColor,Move,Swallow;;!;2;pal=9,sx=100,ix=82,c1=1,c2=0";
static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "PS Attractor@Mass,Particles,Size,Collide,Friction,AgeColor,Move,Swallow;;!;2;pal=9,sx=100,ix=82,c1=2,c2=0";
/*
@@ -8855,7 +9074,6 @@ uint16_t mode_particlespray(void) {
PartSys->sources[0].source.y = map(SEGMENT.custom2, 0, 255, 0, PartSys->maxY);
uint16_t angle = (256 - (((int32_t)SEGMENT.custom3 + 1) << 3)) << 8;
#ifdef USERMOD_AUDIOREACTIVE
um_data_t *um_data;
if (UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { // get AR data, do not use simulated data
uint32_t volumeSmth = (uint8_t)(*(float*) um_data->u_data[0]); //0 to 255
@@ -8879,17 +9097,6 @@ uint16_t mode_particlespray(void) {
PartSys->angleEmit(PartSys->sources[0], angle, SEGMENT.speed >> 2);
}
}
#else
// change source properties
if (SEGMENT.call % (11 - (SEGMENT.intensity / 25)) == 0) { // every nth frame, cycle color and emit particles
PartSys->sources[0].maxLife = 300; // lifetime in frames. note: could be done in init part, but AR moderequires this to be dynamic
PartSys->sources[0].minLife = 100;
PartSys->sources[0].source.hue++; // = hw_random16(); //change hue of spray source
// PartSys->sources[i].var = SEGMENT.custom3; // emiting variation = nozzle size (custom 3 goes from 0-32)
// spray[j].source.hue = hw_random16(); //set random color for each particle (using palette)
PartSys->angleEmit(PartSys->sources[0], angle, SEGMENT.speed >> 2);
}
#endif
PartSys->update(); // update and render
return FRAMETIME;
@@ -9138,6 +9345,7 @@ uint16_t mode_particleblobs(void) {
PartSys->setWallHardness(255);
PartSys->setWallRoughness(255);
PartSys->setCollisionHardness(255);
PartSys->perParticleSize = true; // enable per particle size control
}
else
PartSys = reinterpret_cast<ParticleSystem2D *>(SEGENV.data); // if not first call, just set the pointer to the PS
@@ -9180,16 +9388,14 @@ uint16_t mode_particleblobs(void) {
SEGENV.aux0 = SEGMENT.speed; //write state back
SEGENV.aux1 = SEGMENT.custom1;
#ifdef USERMOD_AUDIOREACTIVE
um_data_t *um_data;
if (UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { // get AR data, do not use simulated data
if (UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { // get AR data if available, do not use simulated data
uint8_t volumeSmth = (uint8_t)(*(float*)um_data->u_data[0]);
for (uint32_t i = 0; i < PartSys->usedParticles; i++) { // update particles
if (SEGMENT.check3) //pulsate selected
PartSys->advPartProps[i].size = volumeSmth;
}
}
#endif
PartSys->setMotionBlur(((SEGMENT.custom3) << 3) + 7);
PartSys->update(); // update and render
@@ -9228,8 +9434,6 @@ uint16_t mode_particlegalaxy(void) {
// Particle System settings
PartSys->updateSystem(); // update system properties (dimensions and data pointers)
uint8_t particlesize = SEGMENT.custom1;
if(SEGMENT.check3)
particlesize = SEGMENT.custom1 ? 1 : 0; // set size to 0 (single pixel) or 1 (quad pixel) so motion blur works and adds streaks
PartSys->setParticleSize(particlesize); // set size globally
PartSys->setMotionBlur(250 * SEGMENT.check3); // adds trails to single/quad pixel particles, no effect if size > 1
@@ -9297,7 +9501,7 @@ uint16_t mode_particlegalaxy(void) {
PartSys->update(); // update and render
return FRAMETIME;
}
static const char _data_FX_MODE_PARTICLEGALAXY[] PROGMEM = "PS Galaxy@!,!,Size,,Color,,Starfield,Trace;;!;2;pal=59,sx=80,c1=2,c3=4";
static const char _data_FX_MODE_PARTICLEGALAXY[] PROGMEM = "PS Galaxy@!,!,Size,,Color,,Starfield,Trace;;!;2;pal=59,sx=80,c1=1,c3=4";
#endif //WLED_DISABLE_PARTICLESYSTEM2D
#endif // WLED_DISABLE_2D
@@ -9400,7 +9604,7 @@ uint16_t mode_particleDrip(void) {
if (PartSys->particles[i].hue < 245)
PartSys->particles[i].hue += 8;
}
//increase speed on high settings by calling the move function twice
//increase speed on high settings by calling the move function twice note: this can lead to missed collisions
if (SEGMENT.speed > 200)
PartSys->particleMoveUpdate(PartSys->particles[i], PartSys->particleFlags[i]);
}
@@ -9412,8 +9616,8 @@ static const char _data_FX_MODE_PARTICLEDRIP[] PROGMEM = "PS DripDrop@Speed,!,Sp
/*
Particle Replacement for "Bbouncing Balls by Aircoookie"
Also replaces rolling balls and juggle (and maybe popcorn)
Particle Version of "Bouncing Balls by Aircoookie"
Also does rolling balls and juggle (and popcorn)
Uses palette for particle color
by DedeHai (Damian Schneider)
*/
@@ -9424,10 +9628,10 @@ uint16_t mode_particlePinball(void) {
if (!initParticleSystem1D(PartSys, 1, 128, 0, true)) // init
return mode_static(); // allocation failed or is single pixel
PartSys->sources[0].sourceFlags.collide = true; // seeded particles will collide (if enabled)
PartSys->sources[0].source.x = PS_P_RADIUS_1D; //emit at bottom
PartSys->setKillOutOfBounds(true); // out of bounds particles dont return
PartSys->sources[0].source.x = -1000; // shoot up from below
//PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting)
SEGENV.aux0 = 1;
SEGENV.aux1 = 5000; //set out of range to ensure uptate on first call
SEGENV.aux1 = 5000; // set settings out of range to ensure uptate on first call
}
else
PartSys = reinterpret_cast<ParticleSystem1D *>(SEGENV.data); // if not first call, just set the pointer to the PS
@@ -9438,62 +9642,71 @@ uint16_t mode_particlePinball(void) {
// Particle System settings
//uint32_t hardness = 240 + (SEGMENT.custom1>>4);
PartSys->updateSystem(); // update system properties (dimensions and data pointers)
PartSys->setGravity(map(SEGMENT.custom3, 0 , 31, 0 , 16)); // set gravity (8 is default strength)
PartSys->setGravity(map(SEGMENT.custom3, 0 , 31, 0 , 8)); // set gravity (8 is default strength)
PartSys->setBounce(SEGMENT.custom3); // disables bounce if no gravity is used
PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur
PartSys->enableParticleCollisions(SEGMENT.check1, 255); // enable collisions and set particle collision to high hardness
PartSys->setUsedParticles(SEGMENT.intensity);
PartSys->setColorByPosition(SEGMENT.check3);
uint32_t maxParticles = max(20, SEGMENT.intensity / (1 + (SEGMENT.check2 * (SEGMENT.custom1 >> 5)))); // max particles depends on intensity and rolling balls mode + size
if (SEGMENT.custom1 < 255)
PartSys->setParticleSize(SEGMENT.custom1); // set size globally
else {
PartSys->perParticleSize = true; // use random individual particle size (see below)
maxParticles *= 2; // use more particles if individual s ize is used as there is more space
}
PartSys->setUsedParticles(maxParticles); // reduce if using larger size and rolling balls mode
bool updateballs = false;
if (SEGENV.aux1 != SEGMENT.speed + SEGMENT.intensity + SEGMENT.check2 + SEGMENT.custom1 + PartSys->usedParticles) { // user settings change or more particles are available
SEGENV.step = SEGMENT.call; // reset delay
updateballs = true;
PartSys->sources[0].maxLife = SEGMENT.custom3 ? 5000 : 0xFFFF; // maximum lifetime in frames/2 (very long if not using gravity, this is enough to travel 4000 pixels at min speed)
PartSys->sources[0].maxLife = SEGMENT.custom3 ? 1000 : 0xFFFF; // maximum lifetime in frames/2 (very long if not using gravity, this is enough to travel 4000 pixels at min speed)
PartSys->sources[0].minLife = PartSys->sources[0].maxLife >> 1;
}
if (SEGMENT.check2) { //rolling balls
if (SEGMENT.check2) { // rolling balls
PartSys->setGravity(0);
PartSys->setWallHardness(255);
int speedsum = 0;
for (uint32_t i = 0; i < PartSys->usedParticles; i++) {
PartSys->particles[i].ttl = 260; // keep particles alive
if (updateballs) { //speed changed or particle is dead, set particle properties
PartSys->particles[i].ttl = 500; // keep particles alive
if (updateballs) { // speed changed or particle is dead, set particle properties
PartSys->particleFlags[i].collide = true;
if (PartSys->particles[i].x == 0) { // still at initial position (when not switching from a PS)
if (PartSys->particles[i].x == 0) { // still at initial position
PartSys->particles[i].x = hw_random16(PartSys->maxX); // random initial position for all particles
PartSys->particles[i].vx = (hw_random16() & 0x01) ? 1 : -1; // random initial direction
}
PartSys->particles[i].hue = hw_random8(); //set ball colors to random
PartSys->advPartProps[i].sat = 255;
PartSys->advPartProps[i].size = SEGMENT.custom1;
PartSys->advPartProps[i].size = hw_random8(); // set ball size for individual size mode
}
speedsum += abs(PartSys->particles[i].vx);
}
int32_t avgSpeed = speedsum / PartSys->usedParticles;
int32_t setSpeed = 2 + (SEGMENT.speed >> 3);
int32_t setSpeed = 2 + (SEGMENT.speed >> 2);
if (avgSpeed < setSpeed) { // if balls are slow, speed up some of them at random to keep the animation going
for (int i = 0; i < setSpeed - avgSpeed; i++) {
int idx = hw_random16(PartSys->usedParticles);
PartSys->particles[idx].vx += PartSys->particles[idx].vx >= 0 ? 1 : -1; // add 1, keep direction
if (abs(PartSys->particles[idx].vx) < PS_P_MAXSPEED)
PartSys->particles[idx].vx += PartSys->particles[idx].vx >= 0 ? 1 : -1; // add 1, keep direction
}
}
else if (avgSpeed > setSpeed + 8) // if avg speed is too high, apply friction to slow them down
PartSys->applyFriction(1);
}
else { //bouncing balls
else { // bouncing balls
PartSys->setWallHardness(220);
PartSys->sources[0].var = SEGMENT.speed >> 3;
int32_t newspeed = 2 + (SEGMENT.speed >> 1) - (SEGMENT.speed >> 3);
PartSys->sources[0].v = newspeed;
//check for balls that are 'laying on the ground' and remove them
for (uint32_t i = 0; i < PartSys->usedParticles; i++) {
if (PartSys->particles[i].vx == 0 && PartSys->particles[i].x < (PS_P_RADIUS_1D + SEGMENT.custom1))
PartSys->particles[i].ttl = 0;
if (PartSys->particles[i].ttl < 50) PartSys->particles[i].ttl = 0; // no dark particles
else if (PartSys->particles[i].vx == 0 && PartSys->particles[i].x < (PS_P_RADIUS_1D + SEGMENT.custom1))
PartSys->particles[i].ttl -= 50; // age fast
if (updateballs) {
PartSys->advPartProps[i].size = SEGMENT.custom1;
if (SEGMENT.custom3 == 0) //gravity off, update speed
if (SEGMENT.custom3 == 0) // gravity off, update speed
PartSys->particles[i].vx = PartSys->particles[i].vx > 0 ? newspeed : -newspeed; //keep the direction
}
}
@@ -9504,14 +9717,14 @@ uint16_t mode_particlePinball(void) {
SEGENV.step += interval + hw_random16(interval);
PartSys->sources[0].source.hue = hw_random16(); //set ball color
PartSys->sources[0].sat = 255;
PartSys->sources[0].size = SEGMENT.custom1;
PartSys->sources[0].size = hw_random8(); //set ball size
PartSys->sprayEmit(PartSys->sources[0]);
}
}
SEGENV.aux1 = SEGMENT.speed + SEGMENT.intensity + SEGMENT.check2 + SEGMENT.custom1 + PartSys->usedParticles;
for (uint32_t i = 0; i < PartSys->usedParticles; i++) {
PartSys->particleMoveUpdate(PartSys->particles[i], PartSys->particleFlags[i]); // double the speed
}
//for (uint32_t i = 0; i < PartSys->usedParticles; i++) {
// PartSys->particleMoveUpdate(PartSys->particles[i], PartSys->particleFlags[i]); // double the speed note: this leads to bad collisions, also need to run collision detection before
//}
PartSys->update(); // update and render
return FRAMETIME;
@@ -9859,7 +10072,7 @@ uint16_t mode_particleHourglass(void) {
PartSys->setUsedParticles(1 + ((SEGMENT.intensity * 255) >> 8));
PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur
PartSys->setGravity(map(SEGMENT.custom3, 0, 31, 1, 30));
PartSys->enableParticleCollisions(true, 32); // hardness value found by experimentation on different settings
PartSys->enableParticleCollisions(true, 64); // hardness value (found by experimentation on different settings)
uint32_t colormode = SEGMENT.custom1 >> 5; // 0-7
@@ -9873,6 +10086,12 @@ uint16_t mode_particleHourglass(void) {
SEGENV.aux0 = PartSys->usedParticles - 1; // initial state, start with highest number particle
}
// re-order particles in case heavy collisions flipped particles (highest number index particle is on the "bottom")
for (uint32_t i = 0; i < PartSys->usedParticles - 1; i++) {
if (PartSys->particles[i].x < PartSys->particles[i+1].x && PartSys->particleFlags[i].fixed == false && PartSys->particleFlags[i+1].fixed == false) {
std::swap(PartSys->particles[i].x, PartSys->particles[i+1].x);
}
}
// calculate target position depending on direction
auto calcTargetPos = [&](size_t i) {
return PartSys->particleFlags[i].reversegrav ?
@@ -9880,12 +10099,12 @@ uint16_t mode_particleHourglass(void) {
: (PartSys->usedParticles - i) * PS_P_RADIUS_1D - positionOffset;
};
for (uint32_t i = 0; i < PartSys->usedParticles; i++) { // check if particle reached target position after falling
if (PartSys->particleFlags[i].fixed == false && abs(PartSys->particles[i].vx) < 5) {
int32_t targetposition = calcTargetPos(i);
bool closeToTarget = abs(targetposition - PartSys->particles[i].x) < 3 * PS_P_RADIUS_1D;
if (closeToTarget) { // close to target and slow speed
bool belowtarget = PartSys->particleFlags[i].reversegrav ? (PartSys->particles[i].x > targetposition) : (PartSys->particles[i].x < targetposition);
bool closeToTarget = abs(targetposition - PartSys->particles[i].x) < PS_P_RADIUS_1D;
if (belowtarget || closeToTarget) { // overshot target or close to target and slow speed
PartSys->particles[i].x = targetposition; // set exact position
PartSys->particleFlags[i].fixed = true; // pin particle
}
@@ -9899,25 +10118,17 @@ uint16_t mode_particleHourglass(void) {
case 0: PartSys->particles[i].hue = 120; break; // fixed at 120, if flip is activated, this can make red and green (use palette 34)
case 1: PartSys->particles[i].hue = basehue; break; // fixed selectable color
case 2: // 2 colors inverleaved (same code as 3)
case 3: PartSys->particles[i].hue = ((SEGMENT.custom1 & 0x1F) << 1) + (i % colormode)*74; break; // interleved colors (every 2 or 3 particles)
case 3: PartSys->particles[i].hue = ((SEGMENT.custom1 & 0x1F) << 1) + (i % 3)*74; break; // 3 interleved colors
case 4: PartSys->particles[i].hue = basehue + (i * 255) / PartSys->usedParticles; break; // gradient palette colors
case 5: PartSys->particles[i].hue = basehue + (i * 1024) / PartSys->usedParticles; break; // multi gradient palette colors
case 6: PartSys->particles[i].hue = i + (strip.now >> 3); break; // disco! moving color gradient
default: break;
default: break; // use color by position
}
}
if (SEGMENT.check1 && !PartSys->particleFlags[i].reversegrav) // flip color when fallen
PartSys->particles[i].hue += 120;
}
// re-order particles in case collisions flipped particles (highest number index particle is on the "bottom")
for (uint32_t i = 0; i < PartSys->usedParticles - 1; i++) {
if (PartSys->particles[i].x < PartSys->particles[i+1].x && PartSys->particleFlags[i].fixed == false && PartSys->particleFlags[i+1].fixed == false) {
std::swap(PartSys->particles[i].x, PartSys->particles[i+1].x);
}
}
if (SEGENV.aux1 == 1) { // last countdown call before dropping starts, reset all particles
for (uint32_t i = 0; i < PartSys->usedParticles; i++) {
PartSys->particleFlags[i].collide = true;
@@ -9929,19 +10140,19 @@ uint16_t mode_particleHourglass(void) {
}
if (SEGENV.aux1 == 0) { // countdown passed, run
if (strip.now >= SEGENV.step) { // drop a particle, do not drop more often than every second frame or particles tangle up quite badly
if (strip.now >= SEGENV.step) { // drop a particle
// set next drop time
if (SEGMENT.check3 && *direction) // fast reset
SEGENV.step = strip.now + 100; // drop one particle every 100ms
else // normal interval
SEGENV.step = strip.now + max(20, SEGMENT.speed * 20); // map speed slider from 0.1s to 5s
SEGENV.step = strip.now + max(100, SEGMENT.speed * 100); // map speed slider from 0.1s to 25.5s
if (SEGENV.aux0 < PartSys->usedParticles) {
PartSys->particleFlags[SEGENV.aux0].reversegrav = *direction; // let this particle fall or rise
PartSys->particleFlags[SEGENV.aux0].fixed = false; // unpin
}
else { // overflow
*direction = !(*direction); // flip direction
SEGENV.aux1 = SEGMENT.virtualLength() + 100; // set countdown
SEGENV.aux1 = (SEGMENT.check2) * SEGMENT.vLength() + 100; // set restart countdown, make it short if auto start is unchecked
}
if (*direction == 0) // down, start dropping the highest number particle
SEGENV.aux0--; // next particle
@@ -9949,14 +10160,14 @@ uint16_t mode_particleHourglass(void) {
SEGENV.aux0++;
}
}
else if (SEGMENT.check2) // auto reset
else if (SEGMENT.check2) // auto start/reset
SEGENV.aux1--; // countdown
PartSys->update(); // update and render
return FRAMETIME;
}
static const char _data_FX_MODE_PS_HOURGLASS[] PROGMEM = "PS Hourglass@Interval,!,Color,Blur,Gravity,Colorflip,Start,Fast Reset;,!;!;1;pal=34,sx=50,ix=200,c1=140,c2=80,c3=4,o1=1,o2=1,o3=1";
static const char _data_FX_MODE_PS_HOURGLASS[] PROGMEM = "PS Hourglass@Interval,!,Color,Blur,Gravity,Colorflip,Start,Fast Reset;,!;!;1;pal=34,sx=5,ix=200,c1=140,c2=80,c3=4,o1=1,o2=1,o3=1";
/*
Particle based Spray effect (like a volcano, possible replacement for popcorn)
@@ -10073,7 +10284,7 @@ uint16_t mode_particleBalance(void) {
}
uint32_t randomindex = hw_random16(PartSys->usedParticles);
PartSys->particles[randomindex].vx = ((int32_t)PartSys->particles[randomindex].vx * 200) / 255; // apply friction to random particle to reduce clumping (without collisions)
PartSys->particles[randomindex].vx = ((int32_t)PartSys->particles[randomindex].vx * 200) / 255; // apply friction to random particle to reduce clumping
//if (SEGMENT.check2 && (SEGMENT.call & 0x07) == 0) // no walls, apply friction to smooth things out
if ((SEGMENT.call & 0x0F) == 0 && SEGMENT.custom3 > 4) // apply friction every 16th frame to smooth things out (except for low tilt)
@@ -10099,7 +10310,7 @@ by DedeHai (Damian Schneider)
uint16_t mode_particleChase(void) {
ParticleSystem1D *PartSys = nullptr;
if (SEGMENT.call == 0) { // initialization
if (!initParticleSystem1D(PartSys, 1, 255, 2, true)) // init
if (!initParticleSystem1D(PartSys, 1, 191, 2, true)) // init
return mode_static(); // allocation failed or is single pixel
SEGENV.aux0 = 0xFFFF; // invalidate
*PartSys->PSdataEnd = 1; // huedir
@@ -10113,15 +10324,17 @@ uint16_t mode_particleChase(void) {
PartSys->updateSystem(); // update system properties (dimensions and data pointers)
PartSys->setColorByPosition(SEGMENT.check3);
PartSys->setMotionBlur(7 + ((SEGMENT.custom3) << 3)); // anable motion blur
uint32_t numParticles = 1 + map(SEGMENT.intensity, 0, 255, 2, 255 / (1 + (SEGMENT.custom1 >> 6))); // depends on intensity and particle size (custom1), minimum 1
uint32_t numParticles = 1 + map(SEGMENT.intensity, 0, 255, 0, PartSys->usedParticles / (1 + (SEGMENT.custom1 >> 5))); // depends on intensity and particle size (custom1), minimum 1
numParticles = min(numParticles, PartSys->usedParticles); // limit to available particles
int32_t huestep = 1 + ((((uint32_t)SEGMENT.custom2 << 19) / numParticles) >> 16); // hue increment
uint32_t settingssum = SEGMENT.speed + SEGMENT.intensity + SEGMENT.custom1 + SEGMENT.custom2 + SEGMENT.check1 + SEGMENT.check2 + SEGMENT.check3;
if (SEGENV.aux0 != settingssum) { // settings changed changed, update
if (SEGMENT.check1)
SEGENV.step = PartSys->advPartProps[0].size / 2 + (PartSys->maxX / numParticles);
else
SEGENV.step = (PartSys->maxX + (PS_P_RADIUS_1D << 5)) / numParticles; // spacing between particles
else {
SEGENV.step = (PartSys->maxX + (PS_P_RADIUS_1D << 6)) / numParticles; // spacing between particles
SEGENV.step = (SEGENV.step / PS_P_RADIUS_1D) * PS_P_RADIUS_1D; // round down to nearest multiple of particle subpixel unit to align to pixel grid (makes them move in union)
}
for (int32_t i = 0; i < (int32_t)PartSys->usedParticles; i++) {
PartSys->advPartProps[i].sat = 255;
PartSys->particles[i].x = (i - 1) * SEGENV.step; // distribute evenly (starts out of frame for i=0)
@@ -10587,7 +10800,7 @@ by DedeHai (Damian Schneider)
uint16_t mode_particleSpringy(void) {
ParticleSystem1D *PartSys = nullptr;
if (SEGMENT.call == 0) { // initialization
if (!initParticleSystem1D(PartSys, 1, 128, 0, true)) // init
if (!initParticleSystem1D(PartSys, 1, 128, 0, true)) // init with advanced properties (used for spring forces)
return mode_static(); // allocation failed or is single pixel
SEGENV.aux0 = SEGENV.aux1 = 0xFFFF; // invalidate settings
}
@@ -10600,18 +10813,20 @@ uint16_t mode_particleSpringy(void) {
PartSys->setMotionBlur(220 * SEGMENT.check1); // anable motion blur
PartSys->setSmearBlur(50); // smear a little
PartSys->setUsedParticles(map(SEGMENT.custom1, 0, 255, 30 >> SEGMENT.check2, 255 >> (SEGMENT.check2*2))); // depends on density and particle size
// PartSys->enableParticleCollisions(true, 140); // enable particle collisions, can not be set too hard or impulses will not strech the springs if soft.
//PartSys->enableParticleCollisions(true, 140); // enable particle collisions, can not be set too hard or impulses will not strech the springs if soft.
int32_t springlength = PartSys->maxX / (PartSys->usedParticles); // spring length (spacing between particles)
int32_t springK = map(SEGMENT.speed, 0, 255, 5, 35); // spring constant (stiffness)
uint32_t settingssum = SEGMENT.custom1 + SEGMENT.check2;
PartSys->setParticleSize(SEGMENT.check2 ? 120 : 1); // large or small particles
if (SEGENV.aux0 != settingssum) { // number of particles changed, update distribution
for (int32_t i = 0; i < (int32_t)PartSys->usedParticles; i++) {
PartSys->advPartProps[i].sat = 255; // full saturation
//PartSys->particleFlags[i].collide = true; // enable collision for particles
//PartSys->particleFlags[i].collide = true; // enable collision for particles -> results in chaos, removed for now
PartSys->particles[i].x = (i+1) * ((PartSys->maxX) / (PartSys->usedParticles)); // distribute
//PartSys->particles[i].vx = 0; //reset speed
PartSys->advPartProps[i].size = SEGMENT.check2 ? 190 : 2; // set size, small or big
//PartSys->advPartProps[i].size = SEGMENT.check2 ? 190 : 2; // set size, small or big -> use global size
}
SEGENV.aux0 = settingssum;
}
@@ -10703,7 +10918,6 @@ uint16_t mode_particleSpringy(void) {
int speed = SEGMENT.custom3 - 10 - (index ? 10 : 0); // map 11-20 and 21-30 to 1-10
int phase = strip.now * ((1 + (SEGMENT.speed >> 4)) * speed);
if (SEGMENT.check2) amplitude <<= 1; // double amplitude for XL particles
//PartSys->applyForce(PartSys->particles[index], (sin16_t(phase) * amplitude) >> 15, PartSys->advPartProps[index].forcecounter); // apply acceleration
PartSys->particles[index].x = restposition + ((sin16_t(phase) * amplitude) >> 12); // apply position
}
else {
@@ -10873,16 +11087,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);
@@ -10909,6 +11125,7 @@ void WS2812FX::setupEffectData() {
addEffect(FX_MODE_BLENDS, &mode_blends, _data_FX_MODE_BLENDS);
addEffect(FX_MODE_TV_SIMULATOR, &mode_tv_simulator, _data_FX_MODE_TV_SIMULATOR);
addEffect(FX_MODE_DYNAMIC_SMOOTH, &mode_dynamic_smooth, _data_FX_MODE_DYNAMIC_SMOOTH);
addEffect(FX_MODE_PACMAN, &mode_pacman, _data_FX_MODE_PACMAN);
// --- 1D audio effects ---
addEffect(FX_MODE_PIXELS, &mode_pixels, _data_FX_MODE_PIXELS);
@@ -10946,7 +11163,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

@@ -310,6 +310,7 @@ extern byte realtimeMode; // used in getMappedPixelIndex()
#define FX_MODE_2DFIRENOISE 149
#define FX_MODE_2DSQUAREDSWIRL 150
// #define FX_MODE_2DFIRE2012 151
#define FX_MODE_PACMAN 151 // gap fill (non-SR). Do NOT renumber; SR-ID range must remain stable.
#define FX_MODE_2DDNA 152
#define FX_MODE_2DMATRIX 153
#define FX_MODE_2DMETABALLS 154
@@ -965,7 +966,8 @@ class WS2812FX {
};
unsigned long now, timebase;
inline uint32_t getPixelColor(unsigned n) const { return (n < getLengthTotal()) ? _pixels[n] : 0; } // returns color of pixel n
inline uint32_t getPixelColor(unsigned n) const { return (getMappedPixelIndex(n) < getLengthTotal()) ? _pixels[n] : 0; } // returns color of pixel n, black if out of (mapped) bounds
inline uint32_t getPixelColorNoMap(unsigned n) const { return (n < getLengthTotal()) ? _pixels[n] : 0; } // ignores mapping table
inline uint32_t getLastShow() const { return _lastShow; } // returns millis() timestamp of last strip.show() call
const char *getModeData(unsigned id = 0) const { return (id && id < _modeCount) ? _modeData[id] : PSTR("Solid"); }

View File

@@ -458,7 +458,7 @@ void Segment::setGeometry(uint16_t i1, uint16_t i2, uint8_t grp, uint8_t spc, ui
return;
}
if (i1 < Segment::maxWidth || (i1 >= Segment::maxWidth*Segment::maxHeight && i1 < strip.getLengthTotal())) start = i1; // Segment::maxWidth equals strip.getLengthTotal() for 1D
stop = i2 > Segment::maxWidth*Segment::maxHeight ? MIN(i2,strip.getLengthTotal()) : constrain(i2, 1, Segment::maxWidth);
stop = i2 > Segment::maxWidth*Segment::maxHeight && i1 >= Segment::maxWidth*Segment::maxHeight ? MIN(i2,strip.getLengthTotal()) : constrain(i2, 1, Segment::maxWidth); // check for 2D trailing strip
startY = 0;
stopY = 1;
#ifndef WLED_DISABLE_2D

File diff suppressed because it is too large Load Diff

View File

@@ -17,7 +17,7 @@
#include <stdint.h>
#include "wled.h"
#define PS_P_MAXSPEED 120 // maximum speed a particle can have (vx/vy is int8)
#define PS_P_MAXSPEED 120 // maximum speed a particle can have (vx/vy is int8), limiting below 127 to avoid overflows in collisions due to rounding errors
#define MAX_MEMIDLE 10 // max idle time (in frames) before memory is deallocated (if deallocated during an effect, it will crash!)
//#define WLED_DEBUG_PS // note: enabling debug uses ~3k of flash
@@ -103,7 +103,7 @@ typedef union {
// struct for additional particle settings (option)
typedef struct { // 2 bytes
uint8_t size; // particle size, 255 means 10 pixels in diameter, 0 means use global size (including single pixel rendering)
uint8_t size; // particle size, 255 means 10 pixels in diameter, set perParticleSize = true to enable
uint8_t forcecounter; // counter for applying forces to individual particles
} PSadvancedParticle;
@@ -190,16 +190,18 @@ public:
int32_t maxXpixel, maxYpixel; // last physical pixel that can be drawn to (FX can read this to read segment size if required), equal to width-1 / height-1
uint32_t numSources; // number of sources
uint32_t usedParticles; // number of particles used in animation, is relative to 'numParticles'
bool perParticleSize; // if true, uses individual particle sizes from advPartProps if available (disabled when calling setParticleSize())
//note: some variables are 32bit for speed and code size at the cost of ram
private:
//rendering functions
void render();
[[gnu::hot]] void renderParticle(const uint32_t particleindex, const uint8_t brightness, const CRGBW& color, const bool wrapX, const bool wrapY);
void renderLargeParticle(const uint32_t size, const uint32_t particleindex, const uint8_t brightness, const CRGBW& color, const bool wrapX, const bool wrapY);
//paricle physics applied by system if flags are set
void applyGravity(); // applies gravity to all particles
void handleCollisions();
[[gnu::hot]] void collideParticles(PSparticle &particle1, PSparticle &particle2, const int32_t dx, const int32_t dy, const uint32_t collDistSq);
void collideParticles(PSparticle &particle1, PSparticle &particle2, int32_t dx, int32_t dy, const uint32_t collDistSq, int32_t massratio1, int32_t massratio2);
void fireParticleupdate();
//utility functions
void updatePSpointers(const bool isadvanced, const bool sizecontrol); // update the data pointers to current segment data space
@@ -226,12 +228,26 @@ private:
uint8_t smearBlur; // 2D smeared blurring of full frame
};
void blur2D(uint32_t *colorbuffer, const uint32_t xsize, uint32_t ysize, const uint32_t xblur, const uint32_t yblur, const uint32_t xstart = 0, uint32_t ystart = 0, const bool isparticle = false);
// initialization functions (not part of class)
bool initParticleSystem2D(ParticleSystem2D *&PartSys, const uint32_t requestedsources, const uint32_t additionalbytes = 0, const bool advanced = false, const bool sizecontrol = false);
uint32_t calculateNumberOfParticles2D(const uint32_t pixels, const bool advanced, const bool sizecontrol);
uint32_t calculateNumberOfSources2D(const uint32_t pixels, const uint32_t requestedsources);
bool allocateParticleSystemMemory2D(const uint32_t numparticles, const uint32_t numsources, const bool advanced, const bool sizecontrol, const uint32_t additionalbytes);
// distance-based brightness for ellipse rendering, returns brightness (0-255) based on distance from ellipse center
inline uint8_t calculateEllipseBrightness(int32_t dx, int32_t dy, int32_t rxsq, int32_t rysq, uint8_t maxBrightness) {
// square the distances
uint32_t dx_sq = dx * dx;
uint32_t dy_sq = dy * dy;
uint32_t dist_sq = ((dx_sq << 8) / rxsq) + ((dy_sq << 8) / rysq); // normalized squared distance in fixed point: (dx²/rx²) * 256 + (dy²/ry²) * 256
if (dist_sq >= 256) return 0; // pixel is outside the ellipse, unit radius in fixed point: 256 = 1.0
//if (dist_sq <= 96) return maxBrightness; // core at full brightness
int32_t falloff = 256 - dist_sq;
return (maxBrightness * falloff) >> 8; // linear falloff
//return (maxBrightness * falloff * falloff) >> 16; // squared falloff for even softer edges
}
#endif // WLED_DISABLE_PARTICLESYSTEM2D
////////////////////////
@@ -301,9 +317,9 @@ typedef union {
// struct for additional particle settings (optional)
typedef struct {
uint8_t sat; //color saturation
uint8_t sat; // color saturation
uint8_t size; // particle size, 255 means 10 pixels in diameter, this overrides global size setting
uint8_t forcecounter;
uint8_t forcecounter; // counter for applying forces to individual particles
} PSadvancedParticle1D;
//struct for a particle source (20 bytes)
@@ -346,10 +362,11 @@ public:
void setColorByPosition(const bool enable);
void setMotionBlur(const uint8_t bluramount); // note: motion blur can only be used if 'particlesize' is set to zero
void setSmearBlur(const uint8_t bluramount); // enable 1D smeared blurring of full frame
void setParticleSize(const uint8_t size); //size 0 = 1 pixel, size 1 = 2 pixels, is overruled if advanced particle is used
void setParticleSize(const uint8_t size); // particle diameter: size 0 = 1 pixel, size 1 = 2 pixels, size = 255 = 10 pixels, disables per particle size control if called
void setGravity(int8_t force = 8);
void enableParticleCollisions(bool enable, const uint8_t hardness = 255);
PSparticle1D *particles; // pointer to particle array
PSparticleFlags1D *particleFlags; // pointer to particle flags array
PSsource1D *sources; // pointer to sources
@@ -360,16 +377,18 @@ public:
int32_t maxXpixel; // last physical pixel that can be drawn to (FX can read this to read segment size if required), equal to width-1
uint32_t numSources; // number of sources
uint32_t usedParticles; // number of particles used in animation, is relative to 'numParticles'
bool perParticleSize; // if true, uses individual particle sizes from advPartProps if available (disabled when calling setParticleSize())
private:
//rendering functions
void render(void);
[[gnu::hot]] void renderParticle(const uint32_t particleindex, const uint8_t brightness, const CRGBW &color, const bool wrap);
void renderParticle(const uint32_t particleindex, const uint8_t brightness, const CRGBW &color, const bool wrap);
void renderLargeParticle(const uint32_t size, const uint32_t particleindex, const uint8_t brightness, const CRGBW& color, const bool wrap);
//paricle physics applied by system if flags are set
void applyGravity(); // applies gravity to all particles
void handleCollisions();
[[gnu::hot]] void collideParticles(PSparticle1D &particle1, const PSparticleFlags1D &particle1flags, PSparticle1D &particle2, const PSparticleFlags1D &particle2flags, const int32_t dx, const uint32_t dx_abs, const uint32_t collisiondistance);
void collideParticles(uint32_t partIdx1, uint32_t partIdx2, int32_t dx, uint32_t collisiondistance);
//utility functions
void updatePSpointers(const bool isadvanced); // update the data pointers to current segment data space
@@ -397,5 +416,5 @@ bool initParticleSystem1D(ParticleSystem1D *&PartSys, const uint32_t requestedso
uint32_t calculateNumberOfParticles1D(const uint32_t fraction, const bool isadvanced);
uint32_t calculateNumberOfSources1D(const uint32_t requestedsources);
bool allocateParticleSystemMemory1D(const uint32_t numparticles, const uint32_t numsources, const bool isadvanced, const uint32_t additionalbytes);
void blur1D(uint32_t *colorbuffer, uint32_t size, uint32_t blur, uint32_t start);
#endif // WLED_DISABLE_PARTICLESYSTEM1D

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
@@ -1016,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
@@ -366,7 +366,8 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit");
#define BTN_TYPE_TOUCH_SWITCH 9
//Ethernet board types
#define WLED_NUM_ETH_TYPES 13
#define WLED_NUM_ETH_TYPES 14
#define WLED_ETH_NONE 0
#define WLED_ETH_WT32_ETH01 1
@@ -381,6 +382,7 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit");
#define WLED_ETH_SERG74 10
#define WLED_ETH_ESP32_POE_WROVER 11
#define WLED_ETH_LILYGO_T_POE_PRO 12
#define WLED_ETH_GLEDOPTO 13
//Hue error codes
#define HUE_ERROR_INACTIVE 0

View File

@@ -51,6 +51,38 @@ function tooltip(cont=null) {
});
});
};
// sequential loading of external resources (JS or CSS) with retry, calls init() when done
function loadResources(files, init) {
let i = 0;
const loadNext = () => {
if (i >= files.length) {
if (init) {
d.documentElement.style.visibility = 'visible'; // make page visible after all files are loaded if it was hidden (prevent ugly display)
d.readyState === 'complete' ? init() : window.addEventListener('load', init);
}
return;
}
const file = files[i++];
const isCSS = file.endsWith('.css');
const el = d.createElement(isCSS ? 'link' : 'script');
if (isCSS) {
el.rel = 'stylesheet';
el.href = file;
const st = d.head.querySelector('style');
if (st) d.head.insertBefore(el, st); // insert before any <style> to allow overrides
else d.head.appendChild(el);
} else {
el.src = file;
d.head.appendChild(el);
}
el.onload = () => { loadNext(); };
el.onerror = () => {
i--; // load this file again
setTimeout(loadNext, 100);
};
};
loadNext();
}
// https://www.educative.io/edpresso/how-to-dynamically-load-a-js-file-in-javascript
function loadJS(FILE_URL, async = true, preGetV = undefined, postGetV = undefined) {
let scE = d.createElement("script");
@@ -116,21 +148,22 @@ function uploadFile(fileObj, name) {
fileObj.value = '';
return false;
}
// connect to WebSocket, use parent WS or open new
// connect to WebSocket, use parent WS or open new, callback function gets passed the new WS object
function connectWs(onOpen) {
try {
if (top.window.ws && top.window.ws.readyState === WebSocket.OPEN) {
if (onOpen) onOpen();
return top.window.ws;
}
} catch (e) {}
getLoc(); // ensure globals (loc, locip, locproto) are up to date
let url = loc ? getURL('/ws').replace("http","ws") : "ws://"+window.location.hostname+"/ws";
let ws = new WebSocket(url);
ws.binaryType = "arraybuffer";
if (onOpen) { ws.onopen = onOpen; }
try { top.window.ws = ws; } catch (e) {} // store in parent for reuse
let ws;
try { ws = top.window.ws;} catch (e) {}
// reuse if open
if (ws && ws.readyState === WebSocket.OPEN) {
if (onOpen) onOpen(ws);
} else {
// create new ws connection
getLoc(); // ensure globals are up to date
let url = loc ? getURL('/ws').replace("http", "ws")
: "ws://" + window.location.hostname + "/ws";
ws = new WebSocket(url);
ws.binaryType = "arraybuffer";
if (onOpen) ws.onopen = () => onOpen(ws);
}
return ws;
}

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);
@@ -1476,7 +1476,7 @@ dialog {
.expanded {
display: inline-block !important;
}
.hide, .expanded .segin.hide, .expanded .presin.hide, .expanded .sbs.hide, .expanded .frz, .expanded .g-icon {
.hide, .expanded .segin.hide, .expanded .presin.hide, .expanded .sbs.hide, .expanded .g-icon {
display: none !important;
}

View File

@@ -128,7 +128,7 @@
</div>
<div style="padding: 8px 0;" id="btns">
<button class="btn btn-xs" title="File editor" type="button" id="edit" onclick="window.location.href=getURL('/edit')"><i class="icons btn-icon">&#xe2c6;</i></button>
<button class="btn btn-xs" title="Pixel Magic Tool" type="button" id="pxmb" onclick="window.location.href=getURL('/pxmagic.htm')"><i class="icons btn-icon">&#xe410;</i></button>
<button class="btn btn-xs" title="PixelForge" type="button" onclick="window.location.href=getURL('/pixelforge.htm')"><i class="icons btn-icon">&#xe410;</i></button>
<button class="btn btn-xs" title="Add custom palette" type="button" id="adPal" onclick="window.location.href=getURL('/cpal.htm')"><i class="icons btn-icon">&#xe18a;</i></button>
<button class="btn btn-xs" title="Remove last custom palette" type="button" id="rmPal" onclick="palettesData=null;localStorage.removeItem('wledPalx');requestJson({rmcpal:true});setTimeout(loadPalettes,250,loadPalettesData);"><i class="icons btn-icon">&#xe037;</i></button>
</div>

View File

@@ -16,13 +16,12 @@ var simplifiedUI = false;
var tr = 7;
var d = document;
const ranges = RangeTouch.setup('input[type="range"]', {});
var retry = false;
var palettesData;
var fxdata = [];
var pJson = {}, eJson = {}, lJson = {};
var plJson = {}; // array of playlists
var pN = "", pI = 0, pNum = 0;
var pmt = 1, pmtLS = 0, pmtLast = 0;
var pmt = 1, pmtLS = 0;
var lastinfo = {};
var isM = false, mw = 0, mh=0;
var ws, wsRpt=0;
@@ -200,19 +199,17 @@ function loadBg() {
});
}
function loadSkinCSS(cId)
{
if (!gId(cId)) // check if element exists
{
var h = d.getElementsByTagName('head')[0];
var l = d.createElement('link');
l.id = cId;
l.rel = 'stylesheet';
l.type = 'text/css';
function loadSkinCSS(cId) {
return new Promise((resolve, reject) => {
if (gId(cId)) return resolve();
const l = d.createElement('link');
l.id = cId;
l.rel = 'stylesheet';
l.href = getURL('/skin.css');
l.media = 'all';
h.appendChild(l);
}
l.onload = resolve;
l.onerror = reject;
d.head.appendChild(l);
});
}
function getURL(path) {
@@ -278,19 +275,23 @@ function onLoad()
cpick.on("color:change", () => {updatePSliders()});
pmtLS = localStorage.getItem('wledPmt');
// Load initial data
loadPalettes(()=>{
// fill effect extra data array
loadFXData(()=>{
// load and populate effects
setTimeout(()=>{loadFX(()=>{
loadPalettesData(()=>{
requestJson();// will load presets and create WS
if (cfg.comp.css) setTimeout(()=>{loadSkinCSS('skinCss')},50);
});
})},50);
});
});
// Load initial data sequentially, no parallel requests to avoid "503" errors when heap is low (slower but much more reliable)
(async ()=>{
try {
await loadPalettes(); // loads base palettes and builds #pallist (safe first)
await loadFXData(); // loads fx data
await loadFX(); // populates effect list
await requestJson(); // updates info variables
await loadPalettesData(); // fills palettesData[] for previews
populatePalettes(); // repopulate with custom palettes now that cpalcount is known
if(pmt == pmtLS) populatePresets(true); // load presets from localStorage if signature matches (i.e. no device reboot)
else await loadPresets(); // load and populate presets
if (cfg.comp.css) await loadSkinCSS('skinCss');
if (!ws) makeWS();
} catch(e) {
showToast("Init failed: " + e, true);
}
})();
resetUtil();
d.addEventListener("visibilitychange", handleVisibilityChange, false);
@@ -448,7 +449,7 @@ function presetError(empty)
if (bckstr.length > 10) hasBackup = true;
} catch (e) {}
var cn = `<div class="pres c" style="padding:8px;margin-bottom:8px;${empty?'':'cursor:pointer;'}" ${empty?'':'onclick="pmtLast=0;loadPresets();"'}>`;
var cn = `<div class="pres c" style="padding:8px;margin-bottom:8px;${empty?'':'cursor:pointer;'}" ${empty?'':'onclick="loadPresets();"'}>`;
if (empty)
cn += `You have no presets yet!`;
else
@@ -481,123 +482,81 @@ function restore(txt) {
return false;
}
function loadPresets(callback = null)
{
// 1st boot (because there is a callback)
if (callback && pmt == pmtLS && pmt > 0) {
// we have a copy of the presets in local storage and don't need to fetch another one
populatePresets(true);
pmtLast = pmt;
callback();
return;
}
// afterwards
if (!callback && pmt == pmtLast) return;
fetch(getURL('/presets.json'), {
method: 'get'
})
.then(res => {
if (res.status=="404") return {"0":{}};
//if (!res.ok) showErrorToast();
return res.json();
})
.then(json => {
pJson = json;
pmtLast = pmt;
populatePresets();
})
.catch((e)=>{
//showToast(e, true);
presetError(false);
})
.finally(()=>{
if (callback) setTimeout(callback,99);
async function loadPresets() {
return new Promise((resolve) => {
fetch(getURL('/presets.json'), {method: 'get'})
.then(res => res.status=="404" ? {"0":{}} : res.json())
.then(json => {
pJson = json;
populatePresets();
resolve();
})
.catch(() => {
presetError(false);
resolve();
})
});
}
function loadPalettes(callback = null)
{
fetch(getURL('/json/palettes'), {
method: 'get'
})
.then((res)=>{
if (!res.ok) showErrorToast();
return res.json();
})
.then((json)=>{
lJson = Object.entries(json);
populatePalettes();
retry = false;
})
.catch((e)=>{
if (!retry) {
retry = true;
setTimeout(loadPalettes, 500); // retry
}
showToast(e, true);
})
.finally(()=>{
if (callback) callback();
updateUI();
async function loadPalettes(retry=0) {
return new Promise((resolve) => {
fetch(getURL('/json/palettes'), {method: 'get'})
.then(res => res.ok ? res.json() : Promise.reject())
.then(json => {
lJson = Object.entries(json);
populatePalettes();
resolve();
})
.catch((e) => {
if (retry<5) {
setTimeout(() => loadPalettes(retry+1).then(resolve), 100);
} else {
showToast(e, true);
resolve();
}
});
});
}
function loadFX(callback = null)
{
fetch(getURL('/json/effects'), {
method: 'get'
})
.then((res)=>{
if (!res.ok) showErrorToast();
return res.json();
})
.then((json)=>{
eJson = Object.entries(json);
populateEffects();
retry = false;
})
.catch((e)=>{
if (!retry) {
retry = true;
setTimeout(loadFX, 500); // retry
}
showToast(e, true);
})
.finally(()=>{
if (callback) callback();
updateUI();
async function loadFX(retry=0) {
return new Promise((resolve) => {
fetch(getURL('/json/effects'), {method: 'get'})
.then(res => res.ok ? res.json() : Promise.reject())
.then(json => {
eJson = Object.entries(json);
populateEffects();
resolve();
})
.catch((e) => {
if (retry<5) {
setTimeout(() => loadFX(retry+1).then(resolve), 100);
} else {
showToast(e, true);
resolve();
}
});
});
}
function loadFXData(callback = null)
{
fetch(getURL('/json/fxdata'), {
method: 'get'
})
.then((res)=>{
if (!res.ok) showErrorToast();
return res.json();
})
.then((json)=>{
fxdata = json||[];
// add default value for Solid
fxdata.shift()
fxdata.unshift(";!;");
retry = false;
})
.catch((e)=>{
fxdata = [];
if (!retry) {
retry = true;
setTimeout(()=>{loadFXData(loadFX);}, 500); // retry
}
showToast(e, true);
})
.finally(()=>{
if (callback) callback();
updateUI();
async function loadFXData(retry=0) {
return new Promise((resolve) => {
fetch(getURL('/json/fxdata'), {method: 'get'})
.then(res => res.ok ? res.json() : Promise.reject())
.then(json => {
fxdata = json||[];
fxdata.shift();
fxdata.unshift(";!;");
resolve();
})
.catch((e) => {
fxdata = [];
if (retry<5) {
setTimeout(() => loadFXData(retry+1).then(resolve), 100);
} else {
showToast(e, true);
resolve();
}
});
});
}
@@ -619,7 +578,7 @@ function populateQL()
function populatePresets(fromls)
{
if (fromls) pJson = JSON.parse(localStorage.getItem("wledP"));
if (!pJson) {setTimeout(loadPresets,250); return;}
if (!pJson) {loadPresets(); return;} // note: no await as this is a fallback that should not be needed as init function fetches pJson
delete pJson["0"];
var cn = "";
var arr = Object.entries(pJson).sort(cmpP);
@@ -693,16 +652,18 @@ 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
//var setInnerHTML = function(elm, html) {
// elm.innerHTML = html;
// Array.from(elm.querySelectorAll("script")).forEach( oldScript => {
// const newScript = document.createElement("script");
// const newScript = d.createElement("script");
// Array.from(oldScript.attributes)
// .forEach( attr => newScript.setAttribute(attr.name, attr.value) );
// newScript.appendChild(document.createTextNode(oldScript.innerHTML));
// newScript.appendChild(d.createTextNode(oldScript.innerHTML));
// oldScript.parentNode.replaceChild(newScript, oldScript);
// });
//}
@@ -904,6 +865,7 @@ function populateSegments(s)
gId("segcont").classList.remove("hide");
let noNewSegs = (lowestUnused >= maxSeg);
resetUtil(noNewSegs);
if (segCount === 0) return; // no segments to populate
for (var i = 0; i <= lSeg; i++) {
if (!gId(`seg${i}`)) continue;
updateLen(i);
@@ -1274,7 +1236,6 @@ function updateUI()
gId('buttonPower').className = (isOn) ? 'active':'';
gId('buttonNl').className = (nlA) ? 'active':'';
gId('buttonSync').className = (syncSend) ? 'active':'';
gId('pxmb').style.display = (isM) ? "inline-block" : "none";
updateSelectedFx();
updateSelectedPalette(selectedPal); // must be after updateSelectedFx() to un-hide color slots for * palettes
@@ -1436,7 +1397,7 @@ function makeWS() {
};
ws.onclose = (e)=>{
gId('connind').style.backgroundColor = "var(--c-r)";
if (wsRpt++ < 5) setTimeout(makeWS,1500); // retry WS connection
if (wsRpt++ < 10) setTimeout(makeWS,wsRpt * 200); // retry WS connection
ws = null;
}
ws.onopen = (e)=>{
@@ -1469,6 +1430,7 @@ function readState(s,command=false)
populateSegments(s);
hasRGB = hasWhite = hasCCT = has2D = false;
segLmax = 0; // reset max selected segment length
let i = {};
// determine light capabilities from selected segments
for (let seg of (s.seg||[])) {
@@ -1708,77 +1670,68 @@ function setEffectParameters(idx)
var jsonTimeout;
var reqsLegal = false;
async function requestJson(command=null, retry=0) {
return new Promise((resolve, reject) => {
gId('connind').style.backgroundColor = "var(--c-y)";
if (command && !reqsLegal) {resolve(); return;}
if (!jsonTimeout) jsonTimeout = setTimeout(()=>{if (ws) ws.close(); ws=null; showErrorToast()}, 3000);
function requestJson(command=null)
{
gId('connind').style.backgroundColor = "var(--c-y)";
if (command && !reqsLegal) return; // stop post requests from chrome onchange event on page restore
if (!jsonTimeout) jsonTimeout = setTimeout(()=>{if (ws) ws.close(); ws=null; showErrorToast()}, 3000);
var req = null;
var useWs = (ws && ws.readyState === WebSocket.OPEN);
var type = command ? 'post':'get';
if (command) {
command.v = true; // force complete /json/si API response
command.time = Math.floor(Date.now() / 1000);
var t = gId('tt');
if (t.validity.valid && command.transition==null) {
var tn = parseInt(t.value*10);
if (tn != tr) command.transition = tn;
var useWs = (ws && ws.readyState === WebSocket.OPEN);
var req = null;
if (command) {
command.v = true;
command.time = Math.floor(Date.now() / 1000);
var t = gId('tt');
if (t && t.validity.valid && command.transition==null) {
var tn = parseInt(t.value*10);
if (tn != tr) command.transition = tn;
}
req = JSON.stringify(command);
if (req.length > 1340) useWs = false;
if (req.length > 500 && lastinfo && lastinfo.arch == "esp8266") useWs = false;
}
//command.bs = parseInt(gId('bs').value);
req = JSON.stringify(command);
if (req.length > 1340) useWs = false; // do not send very long requests over websocket
if (req.length > 500 && lastinfo && lastinfo.arch == "esp8266") useWs = false; // esp8266 can only handle 500 bytes
};
if (useWs) {
ws.send(req?req:'{"v":true}');
return;
}
fetch(getURL('/json/si'), {
method: type,
headers: {"Content-Type": "application/json; charset=UTF-8"},
body: req
})
.then(res => {
clearTimeout(jsonTimeout);
jsonTimeout = null;
if (!res.ok) showErrorToast();
return res.json();
})
.then(json => {
lastUpdate = new Date();
clearErrorToast(3000);
gId('connind').style.backgroundColor = "var(--c-g)";
if (!json) { showToast('Empty response', true); return; }
if (json.success) return;
if (json.info) {
let i = json.info;
parseInfo(i);
populatePalettes(i);
if (isInfo) populateInfo(i);
if (simplifiedUI) simplifyUI();
if (useWs) {
ws.send(req?req:'{"v":true}');
resolve();
return;
}
var s = json.state ? json.state : json;
readState(s);
//load presets and open websocket sequentially
if (!pJson || isEmpty(pJson)) setTimeout(()=>{
loadPresets(()=>{
wsRpt = 0;
if (!(ws && ws.readyState === WebSocket.OPEN)) makeWS();
});
},25);
reqsLegal = true;
retry = false;
})
.catch((e)=>{
if (!retry) {
retry = true;
setTimeout(requestJson,500);
}
showToast(e, true);
fetch(getURL('/json/si'), {
method: command ? 'post' : 'get',
headers: {"Content-Type": "application/json; charset=UTF-8"},
body: req
})
.then(res => {
clearTimeout(jsonTimeout);
jsonTimeout = null;
return res.ok ? res.json() : Promise.reject();
})
.then(json => {
lastUpdate = new Date();
clearErrorToast(3000);
gId('connind').style.backgroundColor = "var(--c-g)";
if (!json) { showToast('Empty response', true); resolve(); return; }
if (json.success) {resolve(); return;}
if (json.info) {
parseInfo(json.info);
if (isInfo) populateInfo(json.info);
if (simplifiedUI) simplifyUI();
}
var s = json.state ? json.state : json;
readState(s);
reqsLegal = true;
resolve();
})
.catch((e)=>{
if (retry<10) {
setTimeout(() => requestJson(command,retry+1).then(resolve).catch(reject), retry*50);
} else {
showToast(e, true);
resolve();
}
});
});
}
@@ -2553,7 +2506,7 @@ function saveP(i,pl)
}
populatePresets();
resetPUtil();
setTimeout(()=>{pmtLast=0; loadPresets();}, 750); // force reloading of presets
setTimeout(()=>{loadPresets();}, 750); // force reloading of presets
}
function testPl(i,bt) {
@@ -2819,56 +2772,51 @@ function rSegs()
requestJson(obj);
}
function loadPalettesData(callback = null)
{
if (palettesData) return;
const lsKey = "wledPalx";
var lsPalData = localStorage.getItem(lsKey);
if (lsPalData) {
try {
var d = JSON.parse(lsPalData);
if (d && d.vid == d.vid) {
palettesData = d.p;
if (callback) callback();
return;
}
} catch (e) {}
}
function loadPalettesData() {
return new Promise((resolve) => {
if (palettesData) return resolve(); // already loaded
var lsPalData = localStorage.getItem("wledPalx");
if (lsPalData) {
try {
var d = JSON.parse(lsPalData);
if (d && d.vid == lastinfo.vid) {
palettesData = d.p;
redrawPalPrev();
return resolve();
}
} catch (e) {}
}
palettesData = {};
getPalettesData(0, ()=>{
localStorage.setItem(lsKey, JSON.stringify({
p: palettesData,
vid: lastinfo.vid
}));
redrawPalPrev();
if (callback) setTimeout(callback, 99);
palettesData = {};
getPalettesData(0, () => {
localStorage.setItem("wledPalx", JSON.stringify({
p: palettesData,
vid: lastinfo.vid
}));
redrawPalPrev();
setTimeout(resolve, 99); // delay optional
});
});
}
function getPalettesData(page, callback)
{
fetch(getURL(`/json/palx?page=${page}`), {
method: 'get'
})
.then(res => {
if (!res.ok) showErrorToast();
return res.json();
})
function getPalettesData(page, callback, retry=0) {
fetch(getURL(`/json/palx?page=${page}`), {method: 'get'})
.then(res => res.ok ? res.json() : Promise.reject())
.then(json => {
retry = false;
palettesData = Object.assign({}, palettesData, json.p);
if (page < json.m) setTimeout(()=>{ getPalettesData(page + 1, callback); }, 75);
else callback();
})
.catch((error)=>{
if (!retry) {
retry = true;
setTimeout(()=>{getPalettesData(page,callback);}, 500); // retry
if (retry<5) {
setTimeout(()=>{getPalettesData(page,callback,retry+1);}, 100);
} else {
showToast(error, true);
callback();
}
showToast(error, true);
});
}
/*
function hideModes(txt)
{
@@ -2970,7 +2918,7 @@ function filterFocus(e) {
}
if (e.type === "blur") {
setTimeout(() => {
if (e.target === document.activeElement && document.hasFocus()) return;
if (e.target === d.activeElement && d.hasFocus()) return;
// do not hide if filter is active
if (!c) {
// compute sticky top
@@ -3217,7 +3165,7 @@ function simplifyUI() {
// Create dropdown dialog
function createDropdown(id, buttonText, dialogElements = null) {
// Create dropdown dialog
const dialog = document.createElement("dialog");
const dialog = d.createElement("dialog");
// Move every dialogElement to the dropdown dialog or if none are given, move all children of the element with the given id
if (dialogElements) {
dialogElements.forEach((e) => {
@@ -3230,7 +3178,7 @@ function simplifyUI() {
}
// Create button for the dropdown
const btn = document.createElement("button");
const btn = d.createElement("button");
btn.id = id + "btn";
btn.classList.add("btn");
btn.innerText = buttonText;
@@ -3278,7 +3226,7 @@ function simplifyUI() {
// Hide palette label
gId("pall").style.display = "none";
gId("Colors").insertBefore(document.createElement("br"), gId("pall"));
gId("Colors").insertBefore(d.createElement("br"), gId("pall"));
// Hide effect label
gId("modeLabel").style.display = "none";
@@ -3290,7 +3238,7 @@ function simplifyUI() {
// Hide bottom bar
gId("bot").style.display = "none";
document.documentElement.style.setProperty('--bh', '0px');
d.documentElement.style.setProperty('--bh', '0px');
// Hide other tabs
gId("Effects").style.display = "none";
@@ -3304,6 +3252,197 @@ 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)
repo: infoData.repo // GitHub repository (always present)
};
// Add optional fields if available
if (infoData.psramPresent !== undefined) upgradeData.psramPresent = infoData.psramPresent; // Whether device has PSRAM
if (infoData.psramSize !== undefined) upgradeData.psramSize = infoData.psramSize; // Total PSRAM size in 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,15 @@
position: absolute;
}
</style>
<script src="common.js"></script>
<script>
// load common.js with retry on error
(function common() {
const l = document.createElement('script');
l.src = 'common.js';
l.onload = () => S();
l.onerror = () => setTimeout(common, 100);
document.head.appendChild(l);
})();
var ws;
var tmout = null;
var c;
@@ -31,7 +38,7 @@
ctx.fillRect(Math.round((i - start) * w / skip), 0, Math.ceil(w), c.height);
}
}
function update() { // via HTTP (/json/live)
function update(retry=0) { // via HTTP (/json/live)
if (d.hidden) {
clearTimeout(tmout);
tmout = setTimeout(update, 250);
@@ -53,7 +60,7 @@
.catch((error)=>{
//console.error("Peek HTTP error:",error);
clearTimeout(tmout);
tmout = setTimeout(update, 2500);
if (retry<5) tmout = setTimeout(() => update(retry+1), 2500); // stop endlessly bugging the ESP if resource is not available
})
}
function S() { // Startup function (onload)
@@ -62,10 +69,7 @@
if (window.location.href.indexOf("?ws") == -1) {update(); return;}
// Initialize WebSocket connection
ws = connectWs(function () {
//console.info("Peek WS open");
ws.send('{"lv":true}');
});
ws = connectWs(ws => ws.send('{"lv":true}'));
ws.addEventListener('message', (e) => {
try {
if (toString.call(e.data) === '[object ArrayBuffer]') {
@@ -81,7 +85,7 @@
}
</script>
</head>
<body onload="S()">
<body>
<canvas id="canv"></canvas>
</body>
</html>

View File

@@ -10,11 +10,18 @@
margin: 0;
}
</style>
<script src="common.js"></script>
</head>
<body>
<canvas id="canv"></canvas>
<script>
// load common.js with retry on error
(function common() {
const l = document.createElement('script');
l.src = 'common.js';
l.onload = () => S();
l.onerror = () => setTimeout(common, 100);
document.head.appendChild(l);
})();
var c = document.getElementById('canv');
var leds = "";
var throttled = false;
@@ -22,36 +29,35 @@
c.width = window.innerWidth * 0.98; //remove scroll bars
c.height = window.innerHeight * 0.98; //remove scroll bars
}
setCanvas();
// Check for canvas support
var ctx = c.getContext('2d');
if (ctx) { // Access the rendering context
// use parent WS or open new
var ws = connectWs(()=>{
ws.send('{"lv":true}');
});
ws.addEventListener('message',(e)=>{
try {
if (toString.call(e.data) === '[object ArrayBuffer]') {
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
let pPL = Math.min(c.width / mW, c.height / mH); // pixels per LED (width of circle)
let lOf = Math.floor((c.width - pPL*mW)/2); //left offset (to center matrix)
var i = 4;
for (y=0.5;y<mH;y++) for (x=0.5; x<mW; x++) {
ctx.fillStyle = `rgb(${leds[i]},${leds[i+1]},${leds[i+2]})`;
ctx.beginPath();
ctx.arc(x*pPL+lOf, y*pPL, pPL*0.4, 0, 2 * Math.PI);
ctx.fill();
i+=3;
function S() { // Startup function (onload)
setCanvas();
// Check for canvas support
var ctx = c.getContext('2d');
if (ctx) { // Access the rendering context
ws = connectWs(ws => ws.send('{"lv":true}')); // use parent WS or open new
ws.addEventListener('message',(e)=>{
try {
if (toString.call(e.data) === '[object ArrayBuffer]') {
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
let pPL = Math.min(c.width / mW, c.height / mH); // pixels per LED (width of circle)
let lOf = Math.floor((c.width - pPL*mW)/2); //left offset (to center matrix)
var i = 4;
for (y=0.5;y<mH;y++) for (x=0.5; x<mW; x++) {
ctx.fillStyle = `rgb(${leds[i]},${leds[i+1]},${leds[i+2]})`;
ctx.beginPath();
ctx.arc(x*pPL+lOf, y*pPL, pPL*0.4, 0, 2 * Math.PI);
ctx.fill();
i+=3;
}
}
} catch (err) {
console.error("Peek WS error:",err);
}
} catch (err) {
console.error("Peek WS error:",err);
}
});
});
}
}
// window.resize event listener
window.addEventListener('resize', (e)=>{

View File

@@ -0,0 +1,809 @@
// (c) Dean McNamee <dean@gmail.com>, 2013.
//
// https://github.com/deanm/omggif
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
//
// omggif is a JavaScript implementation of a GIF 89a encoder and decoder,
// including animation and compression. It does not rely on any specific
// underlying system, so should run in the browser, Node, or Plask.
"use strict";
function GifWriter(buf, width, height, gopts) {
var p = 0;
var gopts = gopts === undefined ? { } : gopts;
var loop_count = gopts.loop === undefined ? null : gopts.loop;
var global_palette = gopts.palette === undefined ? null : gopts.palette;
if (width <= 0 || height <= 0 || width > 65535 || height > 65535)
throw new Error("Width/Height invalid.");
function check_palette_and_num_colors(palette) {
var num_colors = palette.length;
if (num_colors < 2 || num_colors > 256 || num_colors & (num_colors-1)) {
throw new Error(
"Invalid code/color length, must be power of 2 and 2 .. 256.");
}
return num_colors;
}
// - Header.
buf[p++] = 0x47; buf[p++] = 0x49; buf[p++] = 0x46; // GIF
buf[p++] = 0x38; buf[p++] = 0x39; buf[p++] = 0x61; // 89a
// Handling of Global Color Table (palette) and background index.
var gp_num_colors_pow2 = 0;
var background = 0;
if (global_palette !== null) {
var gp_num_colors = check_palette_and_num_colors(global_palette);
while (gp_num_colors >>= 1) ++gp_num_colors_pow2;
gp_num_colors = 1 << gp_num_colors_pow2;
--gp_num_colors_pow2;
if (gopts.background !== undefined) {
background = gopts.background;
if (background >= gp_num_colors)
throw new Error("Background index out of range.");
// The GIF spec states that a background index of 0 should be ignored, so
// this is probably a mistake and you really want to set it to another
// slot in the palette. But actually in the end most browsers, etc end
// up ignoring this almost completely (including for dispose background).
if (background === 0)
throw new Error("Background index explicitly passed as 0.");
}
}
// - Logical Screen Descriptor.
// NOTE(deanm): w/h apparently ignored by implementations, but set anyway.
buf[p++] = width & 0xff; buf[p++] = width >> 8 & 0xff;
buf[p++] = height & 0xff; buf[p++] = height >> 8 & 0xff;
// NOTE: Indicates 0-bpp original color resolution (unused?).
buf[p++] = (global_palette !== null ? 0x80 : 0) | // Global Color Table Flag.
gp_num_colors_pow2; // NOTE: No sort flag (unused?).
buf[p++] = background; // Background Color Index.
buf[p++] = 0; // Pixel aspect ratio (unused?).
// - Global Color Table
if (global_palette !== null) {
for (var i = 0, il = global_palette.length; i < il; ++i) {
var rgb = global_palette[i];
buf[p++] = rgb >> 16 & 0xff;
buf[p++] = rgb >> 8 & 0xff;
buf[p++] = rgb & 0xff;
}
}
if (loop_count !== null) { // Netscape block for looping.
if (loop_count < 0 || loop_count > 65535)
throw new Error("Loop count invalid.")
// Extension code, label, and length.
buf[p++] = 0x21; buf[p++] = 0xff; buf[p++] = 0x0b;
// NETSCAPE2.0
buf[p++] = 0x4e; buf[p++] = 0x45; buf[p++] = 0x54; buf[p++] = 0x53;
buf[p++] = 0x43; buf[p++] = 0x41; buf[p++] = 0x50; buf[p++] = 0x45;
buf[p++] = 0x32; buf[p++] = 0x2e; buf[p++] = 0x30;
// Sub-block
buf[p++] = 0x03; buf[p++] = 0x01;
buf[p++] = loop_count & 0xff; buf[p++] = loop_count >> 8 & 0xff;
buf[p++] = 0x00; // Terminator.
}
var ended = false;
this.addFrame = function(x, y, w, h, indexed_pixels, opts) {
if (ended === true) { --p; ended = false; } // Un-end.
opts = opts === undefined ? { } : opts;
// TODO(deanm): Bounds check x, y. Do they need to be within the virtual
// canvas width/height, I imagine?
if (x < 0 || y < 0 || x > 65535 || y > 65535)
throw new Error("x/y invalid.")
if (w <= 0 || h <= 0 || w > 65535 || h > 65535)
throw new Error("Width/Height invalid.")
if (indexed_pixels.length < w * h)
throw new Error("Not enough pixels for the frame size.");
var using_local_palette = true;
var palette = opts.palette;
if (palette === undefined || palette === null) {
using_local_palette = false;
palette = global_palette;
}
if (palette === undefined || palette === null)
throw new Error("Must supply either a local or global palette.");
var num_colors = check_palette_and_num_colors(palette);
// Compute the min_code_size (power of 2), destroying num_colors.
var min_code_size = 0;
while (num_colors >>= 1) ++min_code_size;
num_colors = 1 << min_code_size; // Now we can easily get it back.
var delay = opts.delay === undefined ? 0 : opts.delay;
// From the spec:
// 0 - No disposal specified. The decoder is
// not required to take any action.
// 1 - Do not dispose. The graphic is to be left
// in place.
// 2 - Restore to background color. The area used by the
// graphic must be restored to the background color.
// 3 - Restore to previous. The decoder is required to
// restore the area overwritten by the graphic with
// what was there prior to rendering the graphic.
// 4-7 - To be defined.
// NOTE(deanm): Dispose background doesn't really work, apparently most
// browsers ignore the background palette index and clear to transparency.
var disposal = opts.disposal === undefined ? 0 : opts.disposal;
if (disposal < 0 || disposal > 3) // 4-7 is reserved.
throw new Error("Disposal out of range.");
var use_transparency = false;
var transparent_index = 0;
if (opts.transparent !== undefined && opts.transparent !== null) {
use_transparency = true;
transparent_index = opts.transparent;
if (transparent_index < 0 || transparent_index >= num_colors)
throw new Error("Transparent color index.");
}
if (disposal !== 0 || use_transparency || delay !== 0) {
// - Graphics Control Extension
buf[p++] = 0x21; buf[p++] = 0xf9; // Extension / Label.
buf[p++] = 4; // Byte size.
buf[p++] = disposal << 2 | (use_transparency === true ? 1 : 0);
buf[p++] = delay & 0xff; buf[p++] = delay >> 8 & 0xff;
buf[p++] = transparent_index; // Transparent color index.
buf[p++] = 0; // Block Terminator.
}
// - Image Descriptor
buf[p++] = 0x2c; // Image Seperator.
buf[p++] = x & 0xff; buf[p++] = x >> 8 & 0xff; // Left.
buf[p++] = y & 0xff; buf[p++] = y >> 8 & 0xff; // Top.
buf[p++] = w & 0xff; buf[p++] = w >> 8 & 0xff;
buf[p++] = h & 0xff; buf[p++] = h >> 8 & 0xff;
// NOTE: No sort flag (unused?).
// TODO(deanm): Support interlace.
buf[p++] = using_local_palette === true ? (0x80 | (min_code_size-1)) : 0;
// - Local Color Table
if (using_local_palette === true) {
for (var i = 0, il = palette.length; i < il; ++i) {
var rgb = palette[i];
buf[p++] = rgb >> 16 & 0xff;
buf[p++] = rgb >> 8 & 0xff;
buf[p++] = rgb & 0xff;
}
}
p = GifWriterOutputLZWCodeStream(
buf, p, min_code_size < 2 ? 2 : min_code_size, indexed_pixels);
return p;
};
this.end = function() {
if (ended === false) {
buf[p++] = 0x3b; // Trailer.
ended = true;
}
return p;
};
this.getOutputBuffer = function() { return buf; };
this.setOutputBuffer = function(v) { buf = v; };
this.getOutputBufferPosition = function() { return p; };
this.setOutputBufferPosition = function(v) { p = v; };
}
// Main compression routine, palette indexes -> LZW code stream.
// |index_stream| must have at least one entry.
function GifWriterOutputLZWCodeStream(buf, p, min_code_size, index_stream) {
buf[p++] = min_code_size;
var cur_subblock = p++; // Pointing at the length field.
var clear_code = 1 << min_code_size;
var code_mask = clear_code - 1;
var eoi_code = clear_code + 1;
var next_code = eoi_code + 1;
var cur_code_size = min_code_size + 1; // Number of bits per code.
var cur_shift = 0;
// We have at most 12-bit codes, so we should have to hold a max of 19
// bits here (and then we would write out).
var cur = 0;
function emit_bytes_to_buffer(bit_block_size) {
while (cur_shift >= bit_block_size) {
buf[p++] = cur & 0xff;
cur >>= 8; cur_shift -= 8;
if (p === cur_subblock + 256) { // Finished a subblock.
buf[cur_subblock] = 255;
cur_subblock = p++;
}
}
}
function emit_code(c) {
cur |= c << cur_shift;
cur_shift += cur_code_size;
emit_bytes_to_buffer(8);
}
// I am not an expert on the topic, and I don't want to write a thesis.
// However, it is good to outline here the basic algorithm and the few data
// structures and optimizations here that make this implementation fast.
// The basic idea behind LZW is to build a table of previously seen runs
// addressed by a short id (herein called output code). All data is
// referenced by a code, which represents one or more values from the
// original input stream. All input bytes can be referenced as the same
// value as an output code. So if you didn't want any compression, you
// could more or less just output the original bytes as codes (there are
// some details to this, but it is the idea). In order to achieve
// compression, values greater then the input range (codes can be up to
// 12-bit while input only 8-bit) represent a sequence of previously seen
// inputs. The decompressor is able to build the same mapping while
// decoding, so there is always a shared common knowledge between the
// encoding and decoder, which is also important for "timing" aspects like
// how to handle variable bit width code encoding.
//
// One obvious but very important consequence of the table system is there
// is always a unique id (at most 12-bits) to map the runs. 'A' might be
// 4, then 'AA' might be 10, 'AAA' 11, 'AAAA' 12, etc. This relationship
// can be used for an effecient lookup strategy for the code mapping. We
// need to know if a run has been seen before, and be able to map that run
// to the output code. Since we start with known unique ids (input bytes),
// and then from those build more unique ids (table entries), we can
// continue this chain (almost like a linked list) to always have small
// integer values that represent the current byte chains in the encoder.
// This means instead of tracking the input bytes (AAAABCD) to know our
// current state, we can track the table entry for AAAABC (it is guaranteed
// to exist by the nature of the algorithm) and the next character D.
// Therefor the tuple of (table_entry, byte) is guaranteed to also be
// unique. This allows us to create a simple lookup key for mapping input
// sequences to codes (table indices) without having to store or search
// any of the code sequences. So if 'AAAA' has a table entry of 12, the
// tuple of ('AAAA', K) for any input byte K will be unique, and can be our
// key. This leads to a integer value at most 20-bits, which can always
// fit in an SMI value and be used as a fast sparse array / object key.
// Output code for the current contents of the index buffer.
var ib_code = index_stream[0] & code_mask; // Load first input index.
var code_table = { }; // Key'd on our 20-bit "tuple".
emit_code(clear_code); // Spec says first code should be a clear code.
// First index already loaded, process the rest of the stream.
for (var i = 1, il = index_stream.length; i < il; ++i) {
var k = index_stream[i] & code_mask;
var cur_key = ib_code << 8 | k; // (prev, k) unique tuple.
var cur_code = code_table[cur_key]; // buffer + k.
// Check if we have to create a new code table entry.
if (cur_code === undefined) { // We don't have buffer + k.
// Emit index buffer (without k).
// This is an inline version of emit_code, because this is the core
// writing routine of the compressor (and V8 cannot inline emit_code
// because it is a closure here in a different context). Additionally
// we can call emit_byte_to_buffer less often, because we can have
// 30-bits (from our 31-bit signed SMI), and we know our codes will only
// be 12-bits, so can safely have 18-bits there without overflow.
// emit_code(ib_code);
cur |= ib_code << cur_shift;
cur_shift += cur_code_size;
while (cur_shift >= 8) {
buf[p++] = cur & 0xff;
cur >>= 8; cur_shift -= 8;
if (p === cur_subblock + 256) { // Finished a subblock.
buf[cur_subblock] = 255;
cur_subblock = p++;
}
}
if (next_code === 4096) { // Table full, need a clear.
emit_code(clear_code);
next_code = eoi_code + 1;
cur_code_size = min_code_size + 1;
code_table = { };
} else { // Table not full, insert a new entry.
// Increase our variable bit code sizes if necessary. This is a bit
// tricky as it is based on "timing" between the encoding and
// decoder. From the encoders perspective this should happen after
// we've already emitted the index buffer and are about to create the
// first table entry that would overflow our current code bit size.
if (next_code >= (1 << cur_code_size)) ++cur_code_size;
code_table[cur_key] = next_code++; // Insert into code table.
}
ib_code = k; // Index buffer to single input k.
} else {
ib_code = cur_code; // Index buffer to sequence in code table.
}
}
emit_code(ib_code); // There will still be something in the index buffer.
emit_code(eoi_code); // End Of Information.
// Flush / finalize the sub-blocks stream to the buffer.
emit_bytes_to_buffer(1);
// Finish the sub-blocks, writing out any unfinished lengths and
// terminating with a sub-block of length 0. If we have already started
// but not yet used a sub-block it can just become the terminator.
if (cur_subblock + 1 === p) { // Started but unused.
buf[cur_subblock] = 0;
} else { // Started and used, write length and additional terminator block.
buf[cur_subblock] = p - cur_subblock - 1;
buf[p++] = 0;
}
return p;
}
function GifReader(buf) {
var p = 0;
// - Header (GIF87a or GIF89a).
if (buf[p++] !== 0x47 || buf[p++] !== 0x49 || buf[p++] !== 0x46 ||
buf[p++] !== 0x38 || (buf[p++]+1 & 0xfd) !== 0x38 || buf[p++] !== 0x61) {
throw new Error("Invalid GIF 87a/89a header.");
}
// - Logical Screen Descriptor.
var width = buf[p++] | buf[p++] << 8;
var height = buf[p++] | buf[p++] << 8;
var pf0 = buf[p++]; // <Packed Fields>.
var global_palette_flag = pf0 >> 7;
var num_global_colors_pow2 = pf0 & 0x7;
var num_global_colors = 1 << (num_global_colors_pow2 + 1);
var background = buf[p++];
buf[p++]; // Pixel aspect ratio (unused?).
var global_palette_offset = null;
var global_palette_size = null;
if (global_palette_flag) {
global_palette_offset = p;
global_palette_size = num_global_colors;
p += num_global_colors * 3; // Seek past palette.
}
var no_eof = true;
var frames = [ ];
var delay = 0;
var transparent_index = null;
var disposal = 0; // 0 - No disposal specified.
var loop_count = null;
this.width = width;
this.height = height;
while (no_eof && p < buf.length) {
switch (buf[p++]) {
case 0x21: // Graphics Control Extension Block
switch (buf[p++]) {
case 0xff: // Application specific block
// Try if it's a Netscape block (with animation loop counter).
if (buf[p ] !== 0x0b || // 21 FF already read, check block size.
// NETSCAPE2.0
buf[p+1 ] == 0x4e && buf[p+2 ] == 0x45 && buf[p+3 ] == 0x54 &&
buf[p+4 ] == 0x53 && buf[p+5 ] == 0x43 && buf[p+6 ] == 0x41 &&
buf[p+7 ] == 0x50 && buf[p+8 ] == 0x45 && buf[p+9 ] == 0x32 &&
buf[p+10] == 0x2e && buf[p+11] == 0x30 &&
// Sub-block
buf[p+12] == 0x03 && buf[p+13] == 0x01 && buf[p+16] == 0) {
p += 14;
loop_count = buf[p++] | buf[p++] << 8;
p++; // Skip terminator.
} else { // We don't know what it is, just try to get past it.
p += 12;
while (true) { // Seek through subblocks.
var block_size = buf[p++];
// Bad block size (ex: undefined from an out of bounds read).
if (!(block_size >= 0)) throw Error("Invalid block size");
if (block_size === 0) break; // 0 size is terminator
p += block_size;
}
}
break;
case 0xf9: // Graphics Control Extension
if (buf[p++] !== 0x4 || buf[p+4] !== 0)
throw new Error("Invalid graphics extension block.");
var pf1 = buf[p++];
delay = buf[p++] | buf[p++] << 8;
transparent_index = buf[p++];
if ((pf1 & 1) === 0) transparent_index = null;
disposal = pf1 >> 2 & 0x7;
p++; // Skip terminator.
break;
case 0xfe: // Comment Extension.
while (true) { // Seek through subblocks.
var block_size = buf[p++];
// Bad block size (ex: undefined from an out of bounds read).
if (!(block_size >= 0)) throw Error("Invalid block size");
if (block_size === 0) break; // 0 size is terminator
// console.log(buf.slice(p, p+block_size).toString('ascii'));
p += block_size;
}
break;
default:
throw new Error(
"Unknown graphic control label: 0x" + buf[p-1].toString(16));
}
break;
case 0x2c: // Image Descriptor.
var x = buf[p++] | buf[p++] << 8;
var y = buf[p++] | buf[p++] << 8;
var w = buf[p++] | buf[p++] << 8;
var h = buf[p++] | buf[p++] << 8;
var pf2 = buf[p++];
var local_palette_flag = pf2 >> 7;
var interlace_flag = pf2 >> 6 & 1;
var num_local_colors_pow2 = pf2 & 0x7;
var num_local_colors = 1 << (num_local_colors_pow2 + 1);
var palette_offset = global_palette_offset;
var palette_size = global_palette_size;
var has_local_palette = false;
if (local_palette_flag) {
var has_local_palette = true;
palette_offset = p; // Override with local palette.
palette_size = num_local_colors;
p += num_local_colors * 3; // Seek past palette.
}
var data_offset = p;
p++; // codesize
while (true) {
var block_size = buf[p++];
// Bad block size (ex: undefined from an out of bounds read).
if (!(block_size >= 0)) throw Error("Invalid block size");
if (block_size === 0) break; // 0 size is terminator
p += block_size;
}
frames.push({x: x, y: y, width: w, height: h,
has_local_palette: has_local_palette,
palette_offset: palette_offset,
palette_size: palette_size,
data_offset: data_offset,
data_length: p - data_offset,
transparent_index: transparent_index,
interlaced: !!interlace_flag,
delay: delay,
disposal: disposal});
break;
case 0x3b: // Trailer Marker (end of file).
no_eof = false;
break;
default:
throw new Error("Unknown gif block: 0x" + buf[p-1].toString(16));
break;
}
}
this.numFrames = function() {
return frames.length;
};
this.loopCount = function() {
return loop_count;
};
this.frameInfo = function(frame_num) {
if (frame_num < 0 || frame_num >= frames.length)
throw new Error("Frame index out of range.");
return frames[frame_num];
}
this.decodeAndBlitFrameBGRA = function(frame_num, pixels) {
var frame = this.frameInfo(frame_num);
var num_pixels = frame.width * frame.height;
var index_stream = new Uint8Array(num_pixels); // At most 8-bit indices.
GifReaderLZWOutputIndexStream(
buf, frame.data_offset, index_stream, num_pixels);
var palette_offset = frame.palette_offset;
// NOTE(deanm): It seems to be much faster to compare index to 256 than
// to === null. Not sure why, but CompareStub_EQ_STRICT shows up high in
// the profile, not sure if it's related to using a Uint8Array.
var trans = frame.transparent_index;
if (trans === null) trans = 256;
// We are possibly just blitting to a portion of the entire frame.
// That is a subrect within the framerect, so the additional pixels
// must be skipped over after we finished a scanline.
var framewidth = frame.width;
var framestride = width - framewidth;
var xleft = framewidth; // Number of subrect pixels left in scanline.
// Output indicies of the top left and bottom right corners of the subrect.
var opbeg = ((frame.y * width) + frame.x) * 4;
var opend = ((frame.y + frame.height) * width + frame.x) * 4;
var op = opbeg;
var scanstride = framestride * 4;
// Use scanstride to skip past the rows when interlacing. This is skipping
// 7 rows for the first two passes, then 3 then 1.
if (frame.interlaced === true) {
scanstride += width * 4 * 7; // Pass 1.
}
var interlaceskip = 8; // Tracking the row interval in the current pass.
for (var i = 0, il = index_stream.length; i < il; ++i) {
var index = index_stream[i];
if (xleft === 0) { // Beginning of new scan line
op += scanstride;
xleft = framewidth;
if (op >= opend) { // Catch the wrap to switch passes when interlacing.
scanstride = framestride * 4 + width * 4 * (interlaceskip-1);
// interlaceskip / 2 * 4 is interlaceskip << 1.
op = opbeg + (framewidth + framestride) * (interlaceskip << 1);
interlaceskip >>= 1;
}
}
if (index === trans) {
op += 4;
} else {
var r = buf[palette_offset + index * 3];
var g = buf[palette_offset + index * 3 + 1];
var b = buf[palette_offset + index * 3 + 2];
pixels[op++] = b;
pixels[op++] = g;
pixels[op++] = r;
pixels[op++] = 255;
}
--xleft;
}
};
// I will go to copy and paste hell one day...
this.decodeAndBlitFrameRGBA = function(frame_num, pixels) {
var frame = this.frameInfo(frame_num);
var num_pixels = frame.width * frame.height;
var index_stream = new Uint8Array(num_pixels); // At most 8-bit indices.
GifReaderLZWOutputIndexStream(
buf, frame.data_offset, index_stream, num_pixels);
var palette_offset = frame.palette_offset;
// NOTE(deanm): It seems to be much faster to compare index to 256 than
// to === null. Not sure why, but CompareStub_EQ_STRICT shows up high in
// the profile, not sure if it's related to using a Uint8Array.
var trans = frame.transparent_index;
if (trans === null) trans = 256;
// We are possibly just blitting to a portion of the entire frame.
// That is a subrect within the framerect, so the additional pixels
// must be skipped over after we finished a scanline.
var framewidth = frame.width;
var framestride = width - framewidth;
var xleft = framewidth; // Number of subrect pixels left in scanline.
// Output indicies of the top left and bottom right corners of the subrect.
var opbeg = ((frame.y * width) + frame.x) * 4;
var opend = ((frame.y + frame.height) * width + frame.x) * 4;
var op = opbeg;
var scanstride = framestride * 4;
// Use scanstride to skip past the rows when interlacing. This is skipping
// 7 rows for the first two passes, then 3 then 1.
if (frame.interlaced === true) {
scanstride += width * 4 * 7; // Pass 1.
}
var interlaceskip = 8; // Tracking the row interval in the current pass.
for (var i = 0, il = index_stream.length; i < il; ++i) {
var index = index_stream[i];
if (xleft === 0) { // Beginning of new scan line
op += scanstride;
xleft = framewidth;
if (op >= opend) { // Catch the wrap to switch passes when interlacing.
scanstride = framestride * 4 + width * 4 * (interlaceskip-1);
// interlaceskip / 2 * 4 is interlaceskip << 1.
op = opbeg + (framewidth + framestride) * (interlaceskip << 1);
interlaceskip >>= 1;
}
}
if (index === trans) {
op += 4;
} else {
var r = buf[palette_offset + index * 3];
var g = buf[palette_offset + index * 3 + 1];
var b = buf[palette_offset + index * 3 + 2];
pixels[op++] = r;
pixels[op++] = g;
pixels[op++] = b;
pixels[op++] = 255;
}
--xleft;
}
};
}
function GifReaderLZWOutputIndexStream(code_stream, p, output, output_length) {
var min_code_size = code_stream[p++];
var clear_code = 1 << min_code_size;
var eoi_code = clear_code + 1;
var next_code = eoi_code + 1;
var cur_code_size = min_code_size + 1; // Number of bits per code.
// NOTE: This shares the same name as the encoder, but has a different
// meaning here. Here this masks each code coming from the code stream.
var code_mask = (1 << cur_code_size) - 1;
var cur_shift = 0;
var cur = 0;
var op = 0; // Output pointer.
var subblock_size = code_stream[p++];
// TODO(deanm): Would using a TypedArray be any faster? At least it would
// solve the fast mode / backing store uncertainty.
// var code_table = Array(4096);
var code_table = new Int32Array(4096); // Can be signed, we only use 20 bits.
var prev_code = null; // Track code-1.
while (true) {
// Read up to two bytes, making sure we always 12-bits for max sized code.
while (cur_shift < 16) {
if (subblock_size === 0) break; // No more data to be read.
cur |= code_stream[p++] << cur_shift;
cur_shift += 8;
if (subblock_size === 1) { // Never let it get to 0 to hold logic above.
subblock_size = code_stream[p++]; // Next subblock.
} else {
--subblock_size;
}
}
// TODO(deanm): We should never really get here, we should have received
// and EOI.
if (cur_shift < cur_code_size)
break;
var code = cur & code_mask;
cur >>= cur_code_size;
cur_shift -= cur_code_size;
// TODO(deanm): Maybe should check that the first code was a clear code,
// at least this is what you're supposed to do. But actually our encoder
// now doesn't emit a clear code first anyway.
if (code === clear_code) {
// We don't actually have to clear the table. This could be a good idea
// for greater error checking, but we don't really do any anyway. We
// will just track it with next_code and overwrite old entries.
next_code = eoi_code + 1;
cur_code_size = min_code_size + 1;
code_mask = (1 << cur_code_size) - 1;
// Don't update prev_code ?
prev_code = null;
continue;
} else if (code === eoi_code) {
break;
}
// We have a similar situation as the decoder, where we want to store
// variable length entries (code table entries), but we want to do in a
// faster manner than an array of arrays. The code below stores sort of a
// linked list within the code table, and then "chases" through it to
// construct the dictionary entries. When a new entry is created, just the
// last byte is stored, and the rest (prefix) of the entry is only
// referenced by its table entry. Then the code chases through the
// prefixes until it reaches a single byte code. We have to chase twice,
// first to compute the length, and then to actually copy the data to the
// output (backwards, since we know the length). The alternative would be
// storing something in an intermediate stack, but that doesn't make any
// more sense. I implemented an approach where it also stored the length
// in the code table, although it's a bit tricky because you run out of
// bits (12 + 12 + 8), but I didn't measure much improvements (the table
// entries are generally not the long). Even when I created benchmarks for
// very long table entries the complexity did not seem worth it.
// The code table stores the prefix entry in 12 bits and then the suffix
// byte in 8 bits, so each entry is 20 bits.
var chase_code = code < next_code ? code : prev_code;
// Chase what we will output, either {CODE} or {CODE-1}.
var chase_length = 0;
var chase = chase_code;
while (chase > clear_code) {
chase = code_table[chase] >> 8;
++chase_length;
}
var k = chase;
var op_end = op + chase_length + (chase_code !== code ? 1 : 0);
if (op_end > output_length) {
console.log("Warning, gif stream longer than expected.");
return;
}
// Already have the first byte from the chase, might as well write it fast.
output[op++] = k;
op += chase_length;
var b = op; // Track pointer, writing backwards.
if (chase_code !== code) // The case of emitting {CODE-1} + k.
output[op++] = k;
chase = chase_code;
while (chase_length--) {
chase = code_table[chase];
output[--b] = chase & 0xff; // Write backwards.
chase >>= 8; // Pull down to the prefix code.
}
if (prev_code !== null && next_code < 4096) {
code_table[next_code++] = prev_code << 8 | k;
// TODO(deanm): Figure out this clearing vs code growth logic better. I
// have an feeling that it should just happen somewhere else, for now it
// is awkward between when we grow past the max and then hit a clear code.
// For now just check if we hit the max 12-bits (then a clear code should
// follow, also of course encoded in 12-bits).
if (next_code >= code_mask+1 && cur_code_size < 12) {
++cur_code_size;
code_mask = code_mask << 1 | 1;
}
}
prev_code = code;
}
if (op !== output_length) {
console.log("Warning, gif stream shorter than expected.");
}
return output;
}
// CommonJS.
//try { exports.GifWriter = GifWriter; exports.GifReader = GifReader } catch(e) {}
try { exports.GifWriter = GifWriter; } catch(e) {}

File diff suppressed because it is too large Load Diff

View File

@@ -1977,4 +1977,4 @@
return isValid;
}
</script>
</html>
</html>

View File

@@ -4,17 +4,23 @@
<meta charset="UTF-8">
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
<title>WLED Settings</title>
<script src="common.js" type="text/javascript"></script>
<script>
function S() {
getLoc();
loadJS(getURL('/settings/s.js?p=0'), false); // If we set async false, file is loaded and executed, then next statement is processed
}
// load common.js with retry on error
(function loadFiles() {
const l = document.createElement('script');
l.src = 'common.js';
// load style.css then initialize
l.onload = () => loadResources(['style.css'], () => {
getLoc();
loadJS(getURL('/settings/s.js?p=0'), false);
});
l.onerror = () => setTimeout(loadFiles, 100);
document.head.appendChild(l);
})();
</script>
<style>
@import url("style.css");
body {
text-align: center;
background: #222;
height: 100px;
margin: 0;
}
@@ -22,21 +28,17 @@
--h: 9vh;
}
button {
background: #333;
color: #fff;
font-family: Verdana, Helvetica, sans-serif;
display: block;
border: 1px solid #333;
border-radius: var(--h);
font-size: 6vmin;
height: var(--h);
width: calc(100% - 40px);
margin: 2vh auto 0;
cursor: pointer;
padding: 0;
}
</style>
</head>
<body onload="S()">
<body>
<button type=submit id="b" onclick="window.location=getURL('/')">Back</button>
<button type="submit" onclick="window.location=getURL('/settings/wifi')">WiFi Setup</button>
<button type="submit" onclick="window.location=getURL('/settings/leds')">LED Preferences</button>

View File

@@ -4,11 +4,20 @@
<meta charset="utf-8">
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
<title>2D Set-up</title>
<script src="common.js" type="text/javascript"></script>
<style> html { visibility: hidden; } </style> <!-- prevent white & ugly display while loading, unhidden in loadResources() -->
<script>
var maxPanels=64;
var ctx = null;
function fS(){d.Sf.submit();} // <button type=submit> sometimes didn't work
// load common.js with retry on error
(function loadFiles() {
const l = document.createElement('script');
l.src = 'common.js';
l.onload = () => loadResources(['style.css'], S); // load style.css then call S()
l.onerror = () => setTimeout(loadFiles, 100);
document.head.appendChild(l);
})();
function S() {
getLoc();
loadJS(getURL('/settings/s.js?p=10'), false, undefined, ()=>{
@@ -238,9 +247,8 @@ Y: <input name="P${i}Y" type="number" min="0" max="255" value="0" oninput="UI()"
gId("MD").innerHTML = "Matrix Dimensions (W*H=LC): " + maxWidth + " x " + maxHeight + " = " + maxWidth * maxHeight;
}
</script>
<style>@import url("style.css");</style>
</head>
<body onload="S()">
<body>
<form id="form_s" name="Sf" method="post">
<div class="toprow">
<div class="helpB"><button type="button" onclick="H('features/2D')">?</button></div>

View File

@@ -4,8 +4,16 @@
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
<meta charset="utf-8">
<title>DMX Settings</title>
<script src="common.js" type="text/javascript"></script>
<style> html { visibility: hidden; } </style> <!-- prevent white & ugly display while loading, unhidden in loadResources() -->
<script>
// load common.js with retry on error
(function loadFiles() {
const l = document.createElement('script');
l.src = 'common.js';
l.onload = () => loadResources(['style.css'], S); // load style.css then call S()
l.onerror = () => setTimeout(loadFiles, 100);
document.head.appendChild(l);
})();
function HW(){window.open("https://kno.wled.ge/interfaces/dmx-output/");}
function GCH(num) {
gId('dmxchannels').innerHTML += "";
@@ -38,9 +46,8 @@
if (loc) d.Sf.action = getURL('/settings/dmx');
}
</script>
<style>@import url("style.css");</style>
</head>
<body onload="S()">
<body>
<form id="form_s" name="Sf" method="post">
<div class="toprow">
<div class="helpB"><button type="button" onclick="HW()">?</button></div>

View File

@@ -4,9 +4,9 @@
<meta charset="utf-8">
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
<title>LED Settings</title>
<script src="common.js" type="text/javascript"></script>
<style> html { visibility: hidden; } </style> <!-- prevent white & ugly display while loading, unhidden in loadResources() -->
<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
@@ -26,6 +26,15 @@
function numPins(t){ return Math.max(gT(t).t.length, 1); } // type length determines number of GPIO pins
function chrID(x) { return String.fromCharCode((x<10?48:55)+x); }
function toNum(c) { let n=c.charCodeAt(0); return (n>=48 && n<=57)?n-48:(n>=65 && n<=90)?n-55:0; } // convert char (0-9A-Z) to number (0-35)
// load common.js with retry on error
(function loadFiles() {
const l = document.createElement('script');
l.src = 'common.js';
l.onload = () => loadResources(['style.css'], S); // load style.css then call S()
l.onerror = () => setTimeout(loadFiles, 100);
document.head.appendChild(l);
})();
function S() {
getLoc();
loadJS(getURL('/settings/s.js?p=2'), false, ()=>{
@@ -43,7 +52,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 +61,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
@@ -267,10 +277,10 @@
}
// enable/disable LED fields
updateTypeDropdowns(); // restrict bus types in dropdowns to max allowed digital/analog buses
let dC = 0; // count of digital buses (for parallel I2S)
let LTs = d.Sf.querySelectorAll("#mLC select[name^=LT]");
LTs.forEach((s,i)=>{
if (i < LTs.length-1) s.disabled = true; // prevent changing type (as we can't update options)
// is the field a LED type?
var n = s.name.substring(2,3); // bus number (0-Z)
var t = parseInt(s.value);
@@ -447,17 +457,8 @@
{
var o = gEBCN("iST");
var i = o.length;
let disable = (sel,opt) => { sel.querySelectorAll(opt).forEach((o)=>{o.disabled=true;}); }
var f = gId("mLC");
let digitalB = 0, analogB = 0, twopinB = 0, virtB = 0;
f.querySelectorAll("select[name^=LT]").forEach((s)=>{
let t = s.value;
if (isDig(t) && !isD2P(t)) digitalB++;
if (isD2P(t)) twopinB++;
if (isPWM(t)) analogB += numPins(t); // each GPIO is assigned to a channel
if (isVir(t)) virtB++;
});
if ((n==1 && i>=36) || (n==-1 && i==0)) return; // used to be i>=maxB+maxV when virtual buses were limited (now :"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ")
var s = chrID(i);
@@ -467,7 +468,7 @@
var cn = `<div class="iST">
<hr class="sml">
${i+1}:
<select name="LT${s}" onchange="UI(true)"></select><br>
<select name="LT${s}" onchange="updateTypeDropdowns();UI(true)"></select><br>
<div id="abl${s}">
mA/LED: <select name="LAsel${s}" onchange="enLA(this,'${s}');UI();">
<option value="55" selected>55mA (typ. 5V WS281x)</option>
@@ -522,18 +523,15 @@ mA/LED: <select name="LAsel${s}" onchange="enLA(this,'${s}');UI();">
}
});
enLA(d.Sf["LAsel"+s],s); // update LED mA
// disable inappropriate LED types
// temporarily set to virtual (network) type to avoid "same type" exception during dropdown update
let sel = d.getElementsByName("LT"+s)[0];
// 32 & S2 supports mono I2S as well as parallel so we need to take that into account; S3 only supports parallel
let maxDB = maxD - (is32() || isS2() || isS3() ? (!d.Sf["PR"].checked)*8 - (!isS3()) : 0); // adjust max digital buses if parallel I2S is not used
if (digitalB >= maxDB) disable(sel,'option[data-type="D"]'); // NOTE: see isDig()
if (twopinB >= 2) disable(sel,'option[data-type="2P"]'); // NOTE: see isD2P() (we will only allow 2 2pin buses)
disable(sel,`option[data-type^="${'A'.repeat(maxA-analogB+1)}"]`); // NOTE: see isPWM()
sel.value = sel.querySelector('option[data-type="N"]').value;
updateTypeDropdowns(); // update valid bus options including this new one
sel.selectedIndex = sel.querySelector('option:not(:disabled)').index;
updateTypeDropdowns(); // update again for the newly selected type
}
if (n==-1) {
o[--i].remove();--i;
o[i].querySelector("[name^=LT]").disabled = false;
}
gId("+").style.display = (i<35) ? "inline":"none"; // was maxB+maxV-1 when virtual buses were limited (now :"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ")
@@ -600,9 +598,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 +612,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;
@@ -812,10 +826,37 @@ Swap: <select id="xw${s}" name="XW${s}">
}
return opt;
}
// dynamically enforce bus type availability based on current usage
function updateTypeDropdowns() {
let LTs = d.Sf.querySelectorAll("#mLC select[name^=LT]");
let digitalB = 0, analogB = 0, twopinB = 0, virtB = 0;
// count currently used buses
LTs.forEach(sel => {
let t = parseInt(sel.value);
if (isDig(t) && !isD2P(t)) digitalB++;
if (isPWM(t)) analogB += numPins(t);
if (isD2P(t)) twopinB++;
if (isVir(t)) virtB++;
});
// enable/disable type options according to limits in dropdowns
LTs.forEach(sel => {
const curType = parseInt(sel.value);
const disable = (q) => sel.querySelectorAll(q).forEach(o => o.disabled = true);
const enable = (q) => sel.querySelectorAll(q).forEach(o => o.disabled = false);
enable('option'); // reset all first
// max digital buses: ESP32 & S2 support mono I2S as well as parallel so we need to take that into account; S3 only supports parallel
// supported outputs using parallel I2S/mono I2S: S2: 12/5, S3: 12/4, ESP32: 16/9
let maxDB = maxD - ((is32() || isS2() || isS3()) ? (!d.Sf["PR"].checked) * 8 - (!isS3()) : 0); // adjust max digital buses if parallel I2S is not used
// disallow adding more of a type that has reached its limit but allow changing the current type
if (digitalB >= maxDB && !(isDig(curType) && !isD2P(curType))) disable('option[data-type="D"]');
if (twopinB >= 2 && !isD2P(curType)) disable('option[data-type="2P"]');
// Disable PWM types that need more pins than available (accounting for current type's pins if PWM)
disable(`option[data-type^="${'A'.repeat(maxA - analogB + (isPWM(curType)?numPins(curType):0) + 1)}"]`);
});
}
</script>
<style>@import url("style.css");</style>
</head>
<body onload="S()">
<body>
<form id="form_s" name="Sf" method="post">
<div class="toprow">
<div class="helpB"><button type="button" onclick="H('features/settings/#led-settings')">?</button></div>
@@ -867,10 +908,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

@@ -4,8 +4,16 @@
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
<meta charset="utf-8">
<title>Misc Settings</title>
<script src="common.js" type="text/javascript"></script>
<style> html { visibility: hidden; } </style> <!-- prevent white & ugly display while loading, unhidden in loadResources() -->
<script>
// load common.js with retry on error
(function loadFiles() {
const l = document.createElement('script');
l.src = 'common.js';
l.onload = () => loadResources(['style.css'], S); // load style.css then call S()
l.onerror = () => setTimeout(loadFiles, 100);
document.head.appendChild(l);
})();
function U() { window.open(getURL("/update"),"_self"); }
function checkNum(o) {
const specialkeys = ["Backspace", "Tab", "Enter", "Shift", "Control", "Alt", "Pause", "CapsLock", "Escape", "Space", "PageUp", "PageDown", "End", "Home", "ArrowLeft", "ArrowUp", "ArrowRight", "ArrowDown", "Insert", "Delete"];
@@ -30,11 +38,8 @@
if (loc) d.Sf.action = getURL('/settings/sec');
}
</script>
<style>
@import url("style.css");
</style>
</head>
<body onload="S()">
<body>
<form id="form_s" name="Sf" method="post">
<div class="toprow">
<div class="helpB"><button type="button" onclick="H('features/settings/#security-settings')">?</button></div>

View File

@@ -4,8 +4,16 @@
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
<meta charset="utf-8">
<title>Sync Settings</title>
<script src="common.js" type="text/javascript"></script>
<style> html { visibility: hidden; } </style> <!-- prevent white & ugly display while loading, unhidden in loadResources() -->
<script>
// load common.js with retry on error
(function loadFiles() {
const l = document.createElement('script');
l.src = 'common.js';
l.onload = () => loadResources(['style.css'], S); // load style.css then call S()
l.onerror = () => setTimeout(loadFiles, 100);
document.head.appendChild(l);
})();
function adj(){if (d.Sf.DI.value == 6454) {if (d.Sf.EU.value == 1) d.Sf.EU.value = 0;}
else if (d.Sf.DI.value == 5568) {if (d.Sf.DA.value == 0) d.Sf.DA.value = 1; if (d.Sf.EU.value == 0) d.Sf.EU.value = 1;} }
function FC()
@@ -43,9 +51,8 @@
function hideDMXInput(){gId("dmxInput").style.display="none";}
function hideNoDMXInput(){gId("dmxInputOff").style.display="none";}
</script>
<style>@import url("style.css");</style>
</head>
<body onload="S()">
<body>
<form id="form_s" name="Sf" method="post" onsubmit="GC()">
<div class="toprow">
<div class="helpB"><button type="button" onclick="H('interfaces/udp-notifier/')">?</button></div>

View File

@@ -4,10 +4,19 @@
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
<meta charset="utf-8">
<title>Time Settings</title>
<script src="common.js" type="text/javascript"></script>
<style> html { visibility: hidden; } </style> <!-- prevent white & ugly display while loading, unhidden in loadResources() -->
<script>
var el=false;
var ms=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
// load common.js with retry on error
(function loadFiles() {
const l = document.createElement('script');
l.src = 'common.js';
l.onload = () => loadResources(['style.css'], S); // load style.css then call S()
l.onerror = () => setTimeout(loadFiles, 100);
document.head.appendChild(l);
})();
function S() {
getLoc();
loadJS(getURL('/settings/s.js?p=5'), false, ()=>{BTa();}, ()=>{
@@ -119,9 +128,8 @@
if (parseFloat(d.Sf.LN.value)<0) { d.Sf.LNR.value = "W"; d.Sf.LN.value = -1*parseFloat(d.Sf.LN.value); } else d.Sf.LNR.value = "E";
}
</script>
<style>@import url("style.css");</style>
</head>
<body onload="S()">
<body>
<form id="form_s" name="Sf" method="post" onsubmit="Wd()">
<div class="toprow">
<div class="helpB"><button type="button" onclick="H('features/settings/#time-settings')">?</button></div>

View File

@@ -4,10 +4,19 @@
<meta charset="utf-8">
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
<title>UI Settings</title>
<script src="common.js" type="text/javascript"></script>
<style> html { visibility: hidden; } </style> <!-- prevent white & ugly display while loading, unhidden in loadResources() -->
<script>
var initial_ds, initial_st, initial_su, oldUrl;
var sett = null;
// load common.js with retry on error
(function loadFiles() {
const l = document.createElement('script');
l.src = 'common.js';
l.onload = () => loadResources(['style.css'], S); // load style.css then call S()
l.onerror = () => setTimeout(loadFiles, 100);
document.head.appendChild(l);
})();
var l = {
"comp":{
"labels":"Show button labels",
@@ -208,9 +217,8 @@
gId("theme_bg_rnd").checked = false;
}
</script>
<style>@import url("style.css");</style>
</head>
<body onload="S()">
<body>
<form id="form_s" name="Sf" method="post">
<div class="toprow">
<div class="helpB"><button type="button" onclick="H('features/settings/#user-interface-settings')">?</button></div>

View File

@@ -4,12 +4,22 @@
<meta charset="utf-8">
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
<title>Usermod Settings</title>
<script src="common.js" type="text/javascript"></script>
<style> html { visibility: hidden; } </style> <!-- prevent white & ugly display while loading, unhidden in loadResources() -->
<script>
var umCfg = {};
var pins = [], pinO = [], owner;
var urows;
var numM = 0;
// load common.js with retry on error
(function loadFiles() {
const l = document.createElement('script');
l.src = 'common.js';
l.onload = () => loadResources(['style.css'], S); // load style.css then call S()
l.onerror = () => setTimeout(loadFiles, 100);
document.head.appendChild(l);
})();
function S() {
getLoc();
// load settings and insert values into DOM
@@ -269,10 +279,9 @@
if (d.Sf.checkValidity()) d.Sf.submit(); //https://stackoverflow.com/q/37323914
}
</script>
<style>@import url("style.css");</style>
</head>
<body onload="S()">
<body>
<form id="form_s" name="Sf" method="post" onsubmit="svS(event)">
<div class="toprow">
<div class="helpB"><button type="button" onclick="H()">?</button></div>

View File

@@ -4,8 +4,17 @@
<meta charset="utf-8">
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
<title>WiFi Settings</title>
<script src="common.js" type="text/javascript"></script>
<style> html { visibility: hidden; } </style> <!-- prevent white & ugly display while loading, unhidden in loadResources() -->
<script>
// load common.js with retry on error
(function loadFiles() {
const l = document.createElement('script');
l.src = 'common.js';
l.onload = () => loadResources(['style.css'], S); // load style.css then call S()
l.onerror = () => setTimeout(loadFiles, 100);
document.head.appendChild(l);
})();
var scanLoops = 0, preScanSSID = "";
var maxNetworks = 3;
function N() {
@@ -178,9 +187,8 @@ Static subnet mask:<br>
}
</script>
<style>@import url("style.css");</style>
</head>
<body onload="S()">
<body>
<form id="form_s" name="Sf" method="post">
<div class="toprow">
<div class="helpB"><button type="button" onclick="H('features/settings/#wifi-settings')">?</button></div>
@@ -270,6 +278,7 @@ Static subnet mask:<br>
<option value="5">TwilightLord-ESP32</option>
<option value="3">WESP32</option>
<option value="1">WT32-ETH01</option>
<option value="13">Gledopto</option>
</select><br><br>
</div>
<hr>

View File

@@ -36,6 +36,7 @@ button, .btn {
min-width: 48px;
cursor: pointer;
text-decoration: none;
transition: all 0.3s ease;
}
button.sml {
padding: 8px;
@@ -44,6 +45,11 @@ button.sml {
min-width: 40px;
margin: 0 0 0 10px;
}
button:hover, .btn:hover{
background:#555;
border-color:#555;
}
#scan {
margin-top: -10px;
}

View File

@@ -839,8 +839,16 @@ void serializeInfo(JsonObject root)
#endif
root[F("freeheap")] = getFreeHeapSize();
#if defined(BOARD_HAS_PSRAM)
root[F("psram")] = ESP.getFreePsram();
#ifdef ARDUINO_ARCH_ESP32
// Report PSRAM information
bool hasPsram = psramFound();
root[F("psramPresent")] = hasPsram;
if (hasPsram) {
#if defined(BOARD_HAS_PSRAM)
root[F("psram")] = ESP.getFreePsram(); // Free PSRAM in bytes (backward compatibility)
#endif
root[F("psramSize")] = ESP.getPsramSize() / (1024UL * 1024UL); // Total PSRAM size in MB
}
#endif
root[F("uptime")] = millis()/1000 + rolloverMillis*4294967;

View File

@@ -144,7 +144,17 @@ const ethernet_settings ethernetBoards[] = {
18, // eth_mdio,
ETH_PHY_LAN8720, // eth_type,
ETH_CLOCK_GPIO0_OUT // eth_clk_mode
}
},
// Gledopto Series With Ethernet
{
1, // eth_address,
5, // eth_power,
23, // eth_mdc,
33, // eth_mdio,
ETH_PHY_LAN8720, // eth_type,
ETH_CLOCK_GPIO0_IN // eth_clk_mode
},
};
bool initEthernet()

View File

@@ -12,7 +12,20 @@
#ifdef ESP32
constexpr size_t METADATA_OFFSET = 256; // ESP32: metadata appears after Espressif metadata
#define UPDATE_ERROR errorString
const size_t BOOTLOADER_OFFSET = 0x1000;
// Bootloader is at fixed offset 0x1000 (4KB), 0x0000 (0KB), or 0x2000 (8KB), and is typically 32KB
// Bootloader offsets for different MCUs => see https://github.com/wled/WLED/issues/5064
#if defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32C6)
constexpr size_t BOOTLOADER_OFFSET = 0x0000; // esp32-S3, esp32-C3 and (future support) esp32-c6
constexpr size_t BOOTLOADER_SIZE = 0x8000; // 32KB, typical bootloader size
#elif defined(CONFIG_IDF_TARGET_ESP32P4) || defined(CONFIG_IDF_TARGET_ESP32C5)
constexpr size_t BOOTLOADER_OFFSET = 0x2000; // (future support) esp32-P4 and esp32-C5
constexpr size_t BOOTLOADER_SIZE = 0x8000; // 32KB, typical bootloader size
#else
constexpr size_t BOOTLOADER_OFFSET = 0x1000; // esp32 and esp32-s2
constexpr size_t BOOTLOADER_SIZE = 0x8000; // 32KB, typical bootloader size
#endif
#elif defined(ESP8266)
constexpr size_t METADATA_OFFSET = 0x1000; // ESP8266: metadata appears at 4KB offset
#define UPDATE_ERROR getErrorString
@@ -280,9 +293,6 @@ static String bootloaderSHA256HexCache = "";
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;
@@ -292,8 +302,8 @@ void calculateBootloaderSHA256() {
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);
for (uint32_t offset = 0; offset < BOOTLOADER_SIZE; offset += chunkSize) {
size_t readSize = min((size_t)(BOOTLOADER_SIZE - offset), chunkSize);
if (esp_flash_read(NULL, buffer, BOOTLOADER_OFFSET + offset, readSize) == ESP_OK) {
mbedtls_sha256_update(&ctx, buffer, readSize);
}

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;

View File

@@ -1159,60 +1159,62 @@ String computeSHA1(const String& input) {
}
#ifdef ESP32
static String dump_raw_block(esp_efuse_block_t block)
{
const int WORDS = 8; // ESP32: 8×32-bit words per block i.e. 256bits
uint32_t buf[WORDS] = {0};
const esp_efuse_desc_t d = {
.efuse_block = block,
.bit_start = 0,
.bit_count = WORDS * 32
};
const esp_efuse_desc_t* field[2] = { &d, NULL };
esp_err_t err = esp_efuse_read_field_blob(field, buf, WORDS * 32);
if (err != ESP_OK) {
return "";
#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
constexpr auto myBIT_WIDTH = ADC_WIDTH_BIT_13;
#else
constexpr auto myBIT_WIDTH = ADC_WIDTH_BIT_12;
#endif
esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_DB_11, myBIT_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];
}
}
String result = "";
for (const unsigned int i : buf) {
char line[32];
sprintf(line, "0x%08X", i);
result += line;
if (ch.high_curve) {
for (int i = 0; i < 8; i++) {
fp[1] ^= ch.high_curve[i];
}
}
return result;
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 "WLED"
// 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;
uint8_t mac[6];
WiFi.macAddress(mac);
char macStr[18];
sprintf(macStr, "%02x:%02x:%02x:%02x:%02x:%02x", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
// 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
#ifdef ESP8266
String deviceString = String(macStr) + "WLED" + ESP.getFlashChipId();
#else
String deviceString = String(macStr) + "WLED" + ESP.getChipModel() + ESP.getChipRevision();
deviceString += dump_raw_block(EFUSE_BLK0);
deviceString += dump_raw_block(EFUSE_BLK1);
deviceString += dump_raw_block(EFUSE_BLK2);
deviceString += dump_raw_block(EFUSE_BLK3);
#endif
String firstHash = computeSHA1(deviceString);
String firstHash = computeSHA1(generateDeviceFingerprint());
// Second hash: SHA1 of the first hash
String secondHash = computeSHA1(firstHash);

View File

@@ -670,7 +670,6 @@ void WLED::initConnection()
if (!WLED_WIFI_CONFIGURED) {
DEBUG_PRINTLN(F("No connection configured."));
if (!apActive) initAP(); // instantly go to ap mode
return;
} else if (!apActive) {
if (apBehavior == AP_BEHAVIOR_ALWAYS) {
DEBUG_PRINTLN(F("Access point ALWAYS enabled."));

View File

@@ -286,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);
@@ -365,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
@@ -571,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
@@ -639,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);
@@ -806,7 +822,7 @@ WLED_GLOBAL byte timerHours[] _INIT_N(({ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }));
WLED_GLOBAL int8_t timerMinutes[] _INIT_N(({ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }));
WLED_GLOBAL byte timerMacro[] _INIT_N(({ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }));
//weekdays to activate on, bit pattern of arr elem: 0b11111111: sun,sat,fri,thu,wed,tue,mon,validity
WLED_GLOBAL byte timerWeekday[] _INIT_N(({ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 }));
WLED_GLOBAL byte timerWeekday[] _INIT_N(({ 254, 254, 254, 254, 254, 254, 254, 254, 254, 254 }));
//upper 4 bits start, lower 4 bits end month (default 28: start month 1 and end month 12)
WLED_GLOBAL byte timerMonth[] _INIT_N(({28,28,28,28,28,28,28,28}));
WLED_GLOBAL byte timerDay[] _INIT_N(({1,1,1,1,1,1,1,1}));

View File

@@ -9,9 +9,12 @@
#ifdef WLED_ENABLE_PIXART
#include "html_pixart.h"
#endif
#ifndef WLED_DISABLE_PXMAGIC
#ifdef WLED_ENABLE_PXMAGIC
#include "html_pxmagic.h"
#endif
#ifndef WLED_DISABLE_PIXELFORGE
#include "html_pixelforge.h"
#endif
#include "html_cpal.h"
#include "html_edit.h"
@@ -27,6 +30,7 @@ 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";
@@ -66,7 +70,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) {
@@ -226,14 +230,18 @@ void createEditHandler() {
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) {
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 == "list") {
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));
@@ -243,15 +251,15 @@ void createEditHandler() {
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();
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();
@@ -600,12 +608,19 @@ void initServer()
});
#endif
#ifndef WLED_DISABLE_PXMAGIC
#ifdef WLED_ENABLE_PXMAGIC
static const char _pxmagic_htm[] PROGMEM = "/pxmagic.htm";
server.on(_pxmagic_htm, HTTP_GET, [](AsyncWebServerRequest *request) {
handleStaticContent(request, FPSTR(_pxmagic_htm), 200, FPSTR(CONTENT_TYPE_HTML), PAGE_pxmagic, PAGE_pxmagic_length);
});
#endif
#ifndef WLED_DISABLE_PIXELFORGE
static const char _pixelforge_htm[] PROGMEM = "/pixelforge.htm";
server.on(_pixelforge_htm, HTTP_GET, [](AsyncWebServerRequest *request) {
handleStaticContent(request, FPSTR(_pixelforge_htm), 200, FPSTR(CONTENT_TYPE_HTML), PAGE_pixelforge, PAGE_pixelforge_length);
});
#endif
#endif
static const char _cpal_htm[] PROGMEM = "/cpal.htm";

View File

@@ -157,12 +157,6 @@ void sendDataWs(AsyncWebSocketClient * client)
// the following may no longer be necessary as heap management has been fixed by @willmmiles in AWS
size_t heap1 = getFreeHeapSize();
DEBUG_PRINTF_P(PSTR("heap %u\n"), getFreeHeapSize());
#ifdef ESP8266
if (len>heap1) {
DEBUG_PRINTLN(F("Out of memory (WS)!"));
return;
}
#endif
AsyncWebSocketBuffer buffer(len);
#ifdef ESP8266
size_t heap2 = getFreeHeapSize();
@@ -236,7 +230,7 @@ bool sendLiveLedsWs(uint32_t wsClient)
#ifndef WLED_DISABLE_2D
if (strip.isMatrix && n>1 && (i/Segment::maxWidth)%n) i += Segment::maxWidth * (n-1);
#endif
uint32_t c = strip.getPixelColor(i);
uint32_t c = strip.getPixelColor(i); // note: LEDs mapped outside of valid range are set to black
uint8_t r = R(c);
uint8_t g = G(c);
uint8_t b = B(c);

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];